I've written a lot of REST APIs. I've hand-authored OpenAPI specs, generated clients from them, and watched the generated code drift from the actual server within a sprint. I've debugged runtime undefined is not an object errors that a type system should have caught at compile time. tRPC fixes all of this, and the DX improvement is real.
The Core Problem with REST in TypeScript Monorepos
In a Next.js fullstack project, your server and client are in the same repo, often the same package. Yet by default, there's a serialization boundary with zero type enforcement. You define a route that returns { user: User }, and the calling component just does (data as any).user or writes a manual type assertion and hopes for the best.
OpenAPI + codegen is the traditional answer. It works, but it's friction: keep the spec in sync, run the generator, commit the generated client, handle the inevitable drift.
What tRPC Actually Does
tRPC doesn't generate code. It uses TypeScript's type inference across the network call itself. Your router is the source of truth, and the client infers the input and output types from it at the type level — not at runtime.
// server/routers/user.ts
export const userRouter = router({
byId: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
return db.user.findUnique({ where: { id: input.id } });
}),
});
// client component
const { data } = api.user.byId.useQuery({ id: userId });
// data is typed as User | null — no assertion neededChange the return shape on the server, and TypeScript immediately flags every call site that breaks. No codegen step. No drift.
Zod as the Contract
Input validation via Zod does double duty: it's both the runtime validator on the server and the type source for the client. A single schema defines what's allowed in and what TypeScript assumes out.
When to Stick with REST
tRPC is a TypeScript-first, monorepo-first solution. If you're building a public API consumed by external clients in multiple languages, REST (or GraphQL) is the right choice. If you're shipping a mobile app that can't share the router type, tRPC breaks down. And if your team isn't on TypeScript, the whole premise collapses.
But for Next.js fullstack apps with a shared codebase? tRPC removes an entire category of bugs before they can exist.