Eliminating API Drift with tRPC's End-to-End Type Safety
Stop fighting API drift. Learn how tRPC uses TypeScript inference and Zod to synchronize server and client types without manual code generation.
05 Jan 2026, 17:48 UTC

The Synchronization Gap
In traditional REST or GraphQL setups, a common point of failure is the "synchronization gap." This happens when a backend developer changes a field name or a validation rule in the API, but the frontend developer isn't notified. The result is a runtime crash or a silent data failure that only appears once the code hits production.
The goal is to make it impossible to call an API endpoint with the wrong data types. Instead of relying on documentation or manual type definitions that can drift, you can use tRPC to treat your server's router as the single source of truth for your client's types.
How Inference Replaces Code Generation
Unlike GraphQL, which requires a schema file and a build step to generate client-side hooks, tRPC uses TypeScript inference. It exports the type of your server router—not the actual implementation code—to the client.
The client uses a Proxy object to mimic the structure of the server's API. When you type trpc.user.getById.useQuery({ id: '123' }), the IDE isn't guessing; it is reading the exact type definition exported from the backend. If you change the id from a string to a number on the server, the client-side code immediately turns red in your editor.
Integrating Runtime Validation with Zod
TypeScript types disappear at runtime, which means they cannot stop a malicious or malformed HTTP request from hitting your logic. To solve this, tRPC integrates with Zod, a schema declaration and validation library.
By defining a Zod schema for the input, you achieve two things simultaneously: you create a runtime validator that rejects bad data, and you provide the TypeScript type that the client uses for autocomplete.
Example: Implementing a Type-Safe Procedure
Assume a TypeScript monorepo where the server and client share a common directory. The following configuration demonstrates a procedure that validates a user update request.
// server/router.ts
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(),
email: z.string().email(),
age: z.number().min(18),
}))
.mutation(async ({ input }) => {
// 'input' is fully typed as { id: string, email: string, age: number }
return { success: true };
}),
});
// Export ONLY the type of the router for the client
export type AppRouter = typeof appRouter;
On the client side, you initialize the proxy using that AppRouter type:
// client/index.ts
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from '../server/router';
const trpc = createTRPCProxyClient<AppRouter>({
links: [httpBatchLink({ url: 'http://localhost:3000/trpc' })],
});
// This call is type-checked against the Zod schema on the server
await trpc.updateUser.mutate({
id: 'user_1',
email: 'test@example.com',
age: 25, // If this were a string, TypeScript would throw an error
});
Trade-offs and Limitations
While this architecture removes the synchronization gap, it introduces specific constraints:
- Monorepo Dependency: For the client to import the
AppRoutertype, the server and client must typically exist in a monorepo or share a published type package. This is not ideal for decoupled teams using different languages. - Public API Constraints: tRPC is designed for internal APIs. If you are building a public API for third-party developers, they won't have access to your TypeScript types, making the end-to-end safety irrelevant for them.
- Compiler Overhead: In extremely large projects with hundreds of procedures, the TypeScript compiler may slow down as it calculates the complex nested types of the Proxy object.
Verifying the Implementation
To confirm that your type safety is functioning correctly, perform these two checks:
- The Breaking Change Test: Go to your server's Zod schema and change a field name (e.g., change
emailtoemailAddress). Check your client code; themutatecall should immediately show a TypeScript error. - The Network Inspection: Open your browser's Network tab and trigger a request. You will see that despite the function-call syntax in your code, tRPC is sending standard HTTP requests (usually GET or POST) to the server.
If the client continues to compile after a server schema change, verify that you are exporting the AppRouter type and not a concrete instance of the router.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.