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.
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 (opts) => {`
`if (!opts.ctx.user?.isAdmin) {`
`throw new TRPCError({ code: 'UNAUTHORIZED' });`
`}`
`return opts.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';`
`},`
});
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((opts) => {
`if (!opts.ctx.user) {`
`throw new TRPCError({ code: 'UNAUTHORIZED' });`
`}`
`return opts.next({`
`ctx: {`
`...opts.ctx,`
`user: opts.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((opts) => {
`if (!opts.ctx.user) {`
`throw new trpc.TRPCError({ code: 'UNAUTHORIZED' });`
`}`
`return opts.next({`
`ctx: {`
`...opts.ctx,`
``// infers that `user` is non-nullable to downstream procedures``
`user: opts.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;`
`},`
});