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.

ts
// 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-query has a function with the same name, so in this course we call ours createContext — to avoid confusion.

initTRPC: the server blank

ts
// 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.

The path of a call: context, middleware, input validation, resolver, response; errors interrupt the pipeline
The path of a call: context, middleware, input validation, resolver, response; errors interrupt the pipeline

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.