A tRPC server is assembled like a construction set: initTRPC creates a blank, router collects procedures, a procedure is a chain of steps (input validation, middleware, your code), and an adapter connects all of this to HTTP. In this lesson we will go through each part using a notes server — the very one that will become the project at the end of the module.
Context: what every procedure needs
Every request has its own "belongings": a database connection, information about who is signed in, headers. All of this is collected in the context (ctx) — an object that is created anew for each request and passed to every procedure. An analogy: a visitor badge in an office. Each visitor (request) has their own badge, and each employee (procedure) looks at it to understand who they are dealing with.
// server/context.ts
import { db } from './db';
import { getUserId } from './auth'; // your own function: session, JWT, cookie
export async function createContext(opts: { headers: Headers }) {
return {
db,
userId: await getUserId(opts.headers), // string | null
};
}
export type Context = Awaited<ReturnType<typeof createContext>>;What is inside getUserId is up to you: an Auth.js session, a JWT from a header, a cookie. TypeScript takes the context type from the function's return value — you do not need to write it by hand.
💡 In the tRPC documentation for Next.js this function is called
createTRPCContext. But@trpc/tanstack-react-queryhas a function with the same name, so in this course we call ourscreateContext— to avoid confusion.
initTRPC: the server blank
// server/trpc.ts
import { initTRPC } from '@trpc/server';
import type { Context } from './context';
const t = initTRPC.context<Context>().create();
export const router = t.router;
export const publicProcedure = t.procedure;
export const createCallerFactory = t.createCallerFactory;initTRPC.context<Context>().create() creates the t object — a set of building parts. We export only the ones we need: router (collects procedures), publicProcedure (a procedure anyone can call) and createCallerFactory (it will be useful for server-side calls and tests). We do not hand out t itself, so that all files use the same set.
Procedures: query and mutation
A procedure is a conveyor belt: a call passes the steps one by one, and any step can stop it with an error.
A procedure is assembled as a chain: we start with publicProcedure, add steps (.input(...), .use(...)) and finish with .query(...) or .mutation(...) — the resolver, that is, your function.