How tRPC Eliminates API Contract Drift Without Code Generation
tRPC uses TypeScript's infer keyword to derive client types directly from the server router—no code generation, no OpenAPI sync, just shared types that catch contract drift at compile time.
08 Oct 2026, 04:22 UTC

The Problem: Drifting Contracts Between Frontend and Backend
Every full-stack TypeScript team knows the friction: you change a backend endpoint, forget to update the OpenAPI spec, and the frontend breaks in production. Code generators help, but they add build steps, version mismatches, and the nagging feeling that your types are only as fresh as the last npm run generate. tRPC takes a different approach—it uses TypeScript's type inference to make the router definition itself the single source of truth, no generation required.
How Inference Replaces Generation
At the heart of tRPC is the infer keyword. When you define a router on the server, TypeScript can extract the exact input and output shapes of every procedure. The client imports only the type of that router—AppRouter—and tRPC's createTRPCProxyClient builds a fully typed client proxy at compile time. No CLI, no .d.ts files emitted to disk, no runtime reflection.
This works because the router is a plain TypeScript object whose shape encodes the entire API contract. The client never imports server implementation code; it only imports the type. In a monorepo, that type lives in a shared package or is exported directly from the server workspace.
Worked Example: A Typed Mutation with Zod Validation
Here's a minimal server router and the client call that refuses to compile if the payload is wrong.
// packages/server/src/router.ts
import { z } from "zod";
import { createTRPCRouter, publicProcedure } from "../trpc";
export const appRouter = createTRPCRouter({
createPost: publicProcedure
.input(z.object({ title: z.string().min(3), content: z.string() }))
.mutation(async ({ input }) => {
// DB write here
return { id: "1", ...input };
}),
});
export type AppRouter = typeof appRouter;
The client side:
// packages/client/src/api.ts
import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
import type { AppRouter } from "@myorg/server"; // type-only import
export const trpc = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({ url: "http://localhost:3000/trpc" }),
],
});
Now the call site:
// This compiles
await trpc.createPost.mutate({ title: "Hello", content: "World" });
// TypeScript error: Property 'content' is missing
await trpc.createPost.mutate({ title: "Hello" });
Run tsc --noEmit in the client package and the second call fails compilation. The network request is a standard HTTP POST to /trpc/createPost with a JSON body—inspect the Network tab to verify. No generated client code exists to get out of sync.
Where the Trade-offs Appear
This model has sharp edges. First, it only works when both ends are TypeScript. A mobile app in Swift or a third-party integration in Go cannot consume the types directly; you'd still need OpenAPI for them. Second, the router type must be physically accessible to the client—usually via a monorepo (npm/yarn/pnpm workspaces, Turborepo, Nx) or a published type-only package. Third, large routers with hundreds of procedures can slow down the TypeScript language server; splitting routers by domain and using mergeRouters keeps the type graph manageable.
Middleware adds another dimension: typed context. A protectedProcedure that attaches a user object to ctx propagates that type to every downstream procedure, so ctx.user.id is autocomplete-available without casting.
Verify It in Your Repo Today
- Create a minimal monorepo with
packages/serverandpackages/client. - Define a router with one Zod-validated procedure in the server package.
- Export
type AppRouterfrom the server'spackage.jsonexportsfield. - In the client, install
@trpc/client, import the type, and create the proxy client. - Write a call with a missing required field and run
tsc --noEmit—it should fail. - Open the browser dev tools Network tab, make a valid call, and confirm a plain HTTP request fires.
If the compilation error appears and the network payload looks boringly normal, you've confirmed end-to-end type safety without a single generated file.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.