Middleware and Context
Add auth and shared context to procedures.
Middleware and Context is a free TypeScript Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the TypeScript Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
What Is Context
Every tRPC request carries a context: shared data available to all procedures, such as the database client, the request, and the current user. You build it once per request with a createContext function.
export async function createContext({ req }: { req: Request }) {
const token = req.headers.get('authorization');
const user = await getUserFromToken(token);
return { user };
}Typing the Context
tRPC infers the context type from createContext and threads it through every procedure. You initialize the instance with that context type so resolvers see ctx typed correctly.
import { initTRPC } from '@trpc/server';
type Context = Awaited<ReturnType<typeof createContext>>;
const t = initTRPC.context<Context>().create();Reading ctx in a Resolver
Inside any procedure, the context is available as ctx, fully typed. Here a procedure reads the current user from context.
const me = t.procedure.query(({ ctx }) => {
// ctx.user is typed from createContext
return ctx.user;
});What Is Middleware
Middleware runs before a procedure resolver. It can check permissions, log, or modify the context. It either calls next() to continue or throws to reject the request.
const logger = t.middleware(async ({ path, next }) => {
console.log('calling', path);
return next();
});An Auth Middleware
A common middleware enforces authentication. If there is no user in context, it throws an UNAUTHORIZED error; otherwise it continues to the resolver.
import { TRPCError } from '@trpc/server';
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next();
});Narrowing Context With next
Middleware can refine the context type by passing a new ctx to next. After the auth check, the user is guaranteed present, so we narrow user from possibly-null to non-null.
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.user) throw new TRPCError({ code: 'UNAUTHORIZED' });
return next({ ctx: { user: ctx.user } }); // user now non-null
});Building a Protected Procedure
Attach the middleware with .use to create a reusable protected procedure. Any procedure built from it requires authentication and sees a non-null user.
export const protectedProcedure = t.procedure.use(isAuthed);
const secret = protectedProcedure.query(({ ctx }) => {
return 'Hello ' + ctx.user.name; // user is guaranteed
});Public vs Protected
You now have two building blocks: publicProcedure for open endpoints and protectedProcedure for authenticated ones. Choosing the right base documents and enforces access at the type and runtime level.
const ping = publicProcedure.query(() => 'pong');
const account = protectedProcedure.query(({ ctx }) => ctx.user);Typed ctx Propagation
The narrowed context propagates: because protectedProcedure guaranteed ctx.user, every resolver built on it sees the non-null type without re-checking. The middleware encodes the invariant once.
// In any protectedProcedure resolver:
// ctx.user is User, not User | nullComposing Middleware
Middleware composes by chaining .use. You might apply logging, then auth, then a role check. Each runs in order, and each can further narrow the context for the next.
const adminProcedure = protectedProcedure.use(({ ctx, next }) => {
if (ctx.user.role !== 'admin') {
throw new TRPCError({ code: 'FORBIDDEN' });
}
return next();
});The Full Picture
Context supplies per-request data, middleware guards and refines it, and specialized procedures bake those guarantees in. Combined with input schemas and the typed client, you get an API that is safe at runtime and at compile time, end to end.
// createContext -> ctx
// middleware -> auth + narrowing
// protectedProcedure -> guaranteed ctx.user
// typed client -> end-to-end safetyQuick Check
Test your understanding of context and middleware.
Recap
You secured a tRPC API with context and middleware.
createContextbuilds per-request shared data, typed viainitTRPC.context.- Middleware runs before resolvers to guard and refine context.
- Passing
ctxtonextnarrows its type. protectedProcedurebakes in auth so resolvers get a non-null user.
You have completed the end-to-end type safety course.
Frequently asked questions
Is the “Middleware and Context” lesson free?
Yes — the full text of “Middleware and Context” is free to read here on the web, and the TypeScript Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the TypeScript Academy course, upgrade to CoddyKit PRO.
What will I learn in “Middleware and Context”?
Add auth and shared context to procedures. You practise TypeScript Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start TypeScript Academy?
No prior experience is required. TypeScript Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Middleware and Context” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this TypeScript Academy lesson?
Yes. Every TypeScript Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- The tRPC Architecture
- Defining Routers and Procedures
- Client-Server Type Inference
- Middleware and Context