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;},});