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.
tstrpc.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';},}),);
tstrpc.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';},}),);
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.
tstrpc.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';},});
tstrpc.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:
tsinterface Context {// user is nullableuser?: {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;},});
tsinterface Context {// user is nullableuser?: {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.tstsximport * 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 proceduresuser: ctx.user,},});});}
server/createRouter.tstsximport * 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 proceduresuser: 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.
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.
tsconst 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 authreturn next({ ctx: { ...ctx, userId } });}).query('userId', {input: inputSchema,resolve({ ctx }) {return ctx.userId;},});
tsconst 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 authreturn next({ ctx: { ...ctx, userId } });}).query('userId', {input: inputSchema,resolve({ ctx }) {return ctx.userId;},});