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';},}),)
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 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;}});
ts
interface 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.tstsx
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 proceduresuser: ctx.user,},});});}
server/createRouter.tstsx
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 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.
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 authreturn 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 authreturn next({ ctx: { ...ctx, userId }})}).query('userId', {input: inputSchema,resolve({ ctx }) {return ctx.userId;},});