Skip to main content
Version: 9.x

Middlewares

You are able to add middleware(s) to a whole router with the middleware() method. The middleware(s) will wrap the invocation of the procedure and must pass through its return value.

Authorization​

In the example below any call to admin.* will ensure that the user is an "admin" before executing any query or mutation.

ts
trpc
.router<Context>()
.query('foo', {
resolve() {
return 'bar';
},
})
.merge(
'admin.',
trpc
.router<Context>()
.middleware(async ({ ctx, next }) => {
if (!ctx.user?.isAdmin) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next()
})
.query('secretPlace', {
resolve() {
return 'a key';
},
}),
)
ts
trpc
.router<Context>()
.query('foo', {
resolve() {
return 'bar';
},
})
.merge(
'admin.',
trpc
.router<Context>()
.middleware(async ({ ctx, next }) => {
if (!ctx.user?.isAdmin) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next()
})
.query('secretPlace', {
resolve() {
return 'a key';
},
}),
)
tip

See Error Handling to learn more about the TRPCError thrown in the above example.

Logging​

In the example below timings for queries are logged automatically.

ts
trpc
.router<Context>()
.middleware(async ({ path, type, next }) => {
const start = Date.now();
const result = await next();
const durationMs = Date.now() - start;
result.ok
? logMock('OK request timing:', { path, type, durationMs })
: logMock('Non-OK request timing', { path, type, durationMs });
return result;
})
.query('foo', {
resolve() {
return 'bar';
},
})
.query('abc', {
resolve() {
return 'def';
},
})
ts
trpc
.router<Context>()
.middleware(async ({ path, type, next }) => {
const start = Date.now();
const result = await next();
const durationMs = Date.now() - start;
result.ok
? logMock('OK request timing:', { path, type, durationMs })
: logMock('Non-OK request timing', { path, type, durationMs });
return result;
})
.query('foo', {
resolve() {
return 'bar';
},
})
.query('abc', {
resolve() {
return 'def';
},
})

Context Swapping​

A middleware can replace the router's context, and downstream procedures will receive the new context value:

ts
interface Context {
// user is nullable
user?: {
id: string
}
}
trpc
.router<Context>()
.middleware(({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({
ctx: {
...ctx,
user: ctx.user, // user value is known to be non-null now
},
});
})
.query('userId', {
async resolve({ctx}) {
return ctx.user.id;
}
});
ts
interface Context {
// user is nullable
user?: {
id: string
}
}
trpc
.router<Context>()
.middleware(({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({
ctx: {
...ctx,
user: ctx.user, // user value is known to be non-null now
},
});
})
.query('userId', {
async resolve({ctx}) {
return ctx.user.id;
}
});

createProtectedRouter()-helper​

This helper can be used anywhere in your app tree to enforce downstream procedures to be authorized.

server/createRouter.ts
tsx
import * as trpc from "@trpc/server";
import { Context } from "./context";
export function createProtectedRouter() {
return trpc
.router<Context>()
.middleware(({ ctx, next }) => {
if (!ctx.user) {
throw new trpc.TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: {
...ctx,
// infers that `user` is non-nullable to downstream procedures
user: ctx.user,
},
});
});
}
server/createRouter.ts
tsx
import * as trpc from "@trpc/server";
import { Context } from "./context";
export function createProtectedRouter() {
return trpc
.router<Context>()
.middleware(({ ctx, next }) => {
if (!ctx.user) {
throw new trpc.TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: {
...ctx,
// infers that `user` is non-nullable to downstream procedures
user: ctx.user,
},
});
});
}

Raw input​

A middleware can access the raw input that will be passed to a procedure. This can be used for authentication / other preprocessing in the middleware that requires access to the procedure input, and can be especially useful when used in conjunction with Context Swapping.

caution

The rawInput passed to a middleware has not yet been validated by a procedure's input schema / validator, so be careful when using it! Because of this, rawInput has type unknown. For more info see #1059.

ts
const inputSchema = z.object({ userId: z.string() });
trpc
.router<Context>()
.middleware(async ({ next, rawInput, ctx }) => {
const result = inputSchema.safeParse(rawInput);
if (!result.success) throw new TRPCError({ code: "BAD_REQUEST" });
const { userId } = result.data;
// Check user id auth
return next({ ctx: { ...ctx, userId }})
})
.query('userId', {
input: inputSchema,
resolve({ ctx }) {
return ctx.userId;
},
});
ts
const inputSchema = z.object({ userId: z.string() });
trpc
.router<Context>()
.middleware(async ({ next, rawInput, ctx }) => {
const result = inputSchema.safeParse(rawInput);
if (!result.success) throw new TRPCError({ code: "BAD_REQUEST" });
const { userId } = result.data;
// Check user id auth
return next({ ctx: { ...ctx, userId }})
})
.query('userId', {
input: inputSchema,
resolve({ ctx }) {
return ctx.userId;
},
});