Eliminating API Drift with tRPC's Type Inference
Stop fighting API drift. Learn how tRPC uses TypeScript type inference and Zod to create a zero-codegen, end-to-end type-safe contract between your server and client.
26 Mar 2026, 15:47 UTC

The Cost of the 'API Contract'
In traditional full-stack development, the gap between the server's implementation and the client's expectation is where most bugs live. Whether you use OpenAPI or GraphQL, you typically maintain a contract—a schema file or a set of interfaces—that must be synchronized. When a backend engineer changes a field from a string to a number, the client often doesn't find out until a runtime error occurs or a code-generation script is manually triggered.
tRPC solves this by treating the server's router as the single source of truth. Instead of generating code, it exports the TypeScript type of the router. The client then imports this type, allowing the IDE to provide autocomplete and error checking based on the actual server implementation in real-time.
How Inference Replaces Code Generation
tRPC relies on TypeScript's infer keyword and a proxy-based client. On the server, you define your procedures (the equivalent of API endpoints) and their input validation schemas using a library like Zod. Zod is a schema declaration and validation library that allows you to define a shape once and derive both the runtime validator and the TypeScript type from it.
Because the client imports only the AppRouter type—not the actual server code—no server-side logic is bundled into the frontend. The client simply "knows" the shape of the API because the TypeScript compiler analyzes the server's router definition during the build process.
Implementation Example: Type-Safe User Updates
This example assumes a Monorepo structure where the client and server share a TypeScript project. We use tRPC v10+ and Zod for validation.
1. Server Definition (server/router.ts)
Run this in your Node.js/Next.js server environment. You will need @trpc/server and zod installed.
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
export const appRouter = t.router({
updateUser: t.procedure
.input(z.object({
id: z.string(),
age: z.number().min(18),
}))
.mutation(async ({ input }) => {
// Logic to update user in DB
return { success: true, updatedId: input.id };
}),
});
// Export ONLY the type of the router, not the router itself
export type AppRouter = typeof appRouter;
2. Client Consumption (client/api.ts)
Run this in your frontend environment. Import the AppRouter type from the server file.
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from '../server/router';
const trpc = createTRPCProxyClient<AppRouter>({
links: [httpBatchLink({ url: 'http://localhost:3000/trpc' })],
});
// TypeScript will error here if 'age' is passed as a string
// or if the 'id' field is missing.
const result = await trpc.updateUser.mutate({
id: 'user_123',
age: 25,
});
Verification and Diagnostics
To verify the type safety is working, navigate to the server file and change age: z.number() to age: z.string(). Without refreshing or running a build script, your IDE should immediately highlight the trpc.updateUser.mutate call in the client file as a TypeScript error.
To check the actual network behavior, open the Browser DevTools Network tab. You will see that tRPC sends standard HTTP requests (usually GET for queries and POST for mutations) with the procedure name and input serialized as JSON in the URL or body.
The Trade-off: Tight Coupling
The primary limitation of tRPC is that it requires a shared TypeScript environment. Because the client imports the AppRouter type, the client and server must exist in the same repository (Monorepo) or share a published type package. This makes tRPC an excellent choice for internal APIs where you control both ends of the wire, but a poor choice for public-facing APIs where third-party developers cannot import your internal TypeScript types.
Additionally, in extremely large projects with hundreds of procedures, the TypeScript compiler may experience increased memory usage while calculating the deeply nested types of the router proxy.
Actionable Summary
If you are building a full-stack TypeScript application and find yourself manually updating interfaces every time the backend changes, migrate your API layer to tRPC. Start by defining your inputs with Zod to ensure runtime safety, then export the AppRouter type to your frontend to eliminate the need for a separate API documentation or code-generation step.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.