rembrembdocs

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;`

`},`

});