middlewares
coreCreate and compose tRPC middleware with t.procedure.use(), extend context via opts.next({ ctx }), build reusable middleware with .concat() and .unstable_pipe(), define base procedures like publicProcedure and authedProcedure. Access raw input with getRawInput(). Logging, timing, OTEL tracing patterns.
tRPC -- Middlewares
Setup
// server/trpc.ts
import { initTRPC, TRPCError } from '@trpc/server';
type Context = {
user?: { id: string; isAdmin: boolean };
};
const t = initTRPC.context<Context>().create();
export const router = t.router;
export const publicProcedure = t.procedure;
export const middleware = t.middleware;
Core Patterns
Auth middleware that narrows context type
// server/trpc.ts
import { initTRPC, TRPCError } from '@trpc/server';
type Context = {
user?: { id: string; isAdmin: boolean };
};
const t = initTRPC.context<Context>().create();
export const publicProcedure = t.procedure;
export const authedProcedure = t.procedure.use(async (opts) => {
const { ctx } = opts;
if (!ctx.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return opts.next({
ctx: {
user: ctx.user,
},
});
});
export const adminProcedure = t.procedure.use(async (opts) => {
const { ctx } = opts;
if (!ctx.user?.isAdmin) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return opts.next({
ctx: {
user: ctx.user,
},
});
});
After the middleware, ctx.user is non-nullable in downstream procedures.
Logging and timing middleware
// server/trpc.ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const loggedProcedure = t.procedure.use(async (opts) => {
const start = Date.now();
const result = await opts.next();
const durationMs = Date.now() - start;
const meta = { path: opts.path, type: opts.type, durationMs };
result.ok
? console.log('OK request timing:', meta)
: console.error('Non-OK request timing', meta);
return result;
});
Reusable middleware with .concat()
// myPlugin.ts
import { initTRPC } from '@trpc/server';
export function createMyPlugin() {
const t = initTRPC.context<{}>().meta<{}>().create();
return {
pluginProc: t.procedure.use((opts) => {
return opts.next({
ctx: {
fromPlugin: 'hello from myPlugin' as const,
},
});
}),
};
}
// server/trpc.ts
import { initTRPC } from '@trpc/server';
import { createMyPlugin } from './myPlugin';
const t = initTRPC.context<{}>().create();
const plugin = createMyPlugin();
export const publicProcedure = t.procedure;
export const procedureWithPlugin = publicProcedure.concat(plugin.pluginProc);
.concat() merges a partial procedure (from any tRPC instance) into your procedure chain, as long as context and meta types overlap.
Extending middlewares with .unstable_pipe()
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
const fooMiddleware = t.middleware((opts) => {
return opts.next({
ctx: { foo: 'foo' as const },
});
});
const barMiddleware = fooMiddleware.unstable_pipe((opts) => {
console.log(opts.ctx.foo);
return opts.next({
ctx: { bar: 'bar' as const },
});
});
const barProcedure = t.procedure.use(barMiddleware);
Piped middlewares run in order and each receives the context from the previous middleware.
Common Mistakes
[CRITICAL] Forgetting to call and return opts.next()
Wrong:
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
const logMiddleware = t.middleware(async (opts) => {
console.log('request started');
// forgot to call opts.next()
});
Correct:
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
const logMiddleware = t.middleware(async (opts) => {
console.log('request started');
const result = await opts.next();
console.log('request ended');
return result;
});
Middleware must call opts.next() and return its result; forgetting this silently drops the request with an INTERNAL_SERVER_ERROR because no middleware marker is returned.
Source: packages/server/src/unstable-core-do-not-import/procedureBuilder.ts
[HIGH] Extending context with wrong type in opts.next()
Wrong:
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
const middleware = t.middleware(async (opts) => {
return opts.next({ ctx: 'not-an-object' });
});
Correct:
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
async function getUser() {
return { id: '1', name: 'Katt' };
}
const middleware = t.middleware(async (opts) => {
return opts.next({ ctx: { user: await getUser() } });
});
Context extension in opts.next({ ctx }) must be an object; passing non-object values or overwriting required keys breaks downstream procedures.
Source: www/docs/server/middlewares.md
See Also
- server-setup -- initTRPC, routers, procedures, context
- validators -- input/output validation with Zod
- error-handling -- TRPCError codes used in auth middleware
- auth -- full auth patterns combining middleware + client headers