Tutorial: Build an End-to-End Typed API with tRPC and TypeScript
Hands-on tutorial to build a fully typed API using tRPC, TypeScript, and a simple frontend — learn how types flow from DB to UI with zero duplicated schemas.
Tutorial: Build an End-to-End Typed API with tRPC and TypeScript
Overview: tRPC allows you to define API contracts in TypeScript so you get type-safety from database to UI without generating code. This tutorial walks through setting up a minimal tRPC server and connecting a typed React client.
“When types travel with your code, refactors become far safer — and the UI gets far fewer surprises.”
1. Why tRPC?
With REST or GraphQL, you often duplicate types between server and client. tRPC eliminates that duplication by sharing procedure definitions and types via a common package or monorepo. This results in faster feedback loops and fewer contract mismatches.
2. Project scaffolding
For a simple demo, create a monorepo with a server and client package. Use pnpm/yarn workspaces for easy package sharing.
3. Define shared types
// packages/shared/types.ts
export type Todo = {
id: string;
title: string;
completed: boolean;
};
4. Server: create tRPC router
// server/src/router.ts
import {z} from 'zod';
import {initTRPC} from '@trpc/server';
import type {Todo} from 'shared/types';
const t = initTRPC.create();
export const appRouter = t.router({
getTodos: t.procedure.query(() => {
// fake DB
const todos: Todo[] = [{id: '1', title: 'Buy milk', completed: false}];
return todos;
}),
addTodo: t.procedure.input(z.string()).mutation((req) => {
// persist and return new todo
return {id: 'generated', title: req.input, completed: false} as Todo;
})
});
export type AppRouter = typeof appRouter;
5. Client: create typed hooks
On the client, import the AppRouter type and create a proxy to call procedures with type safety:
// client/src/trpc.ts
import {createTRPCProxyClient, httpBatchLink} from '@trpc/client';
import type {AppRouter} from 'server/src/router';
export const trpc = createTRPCProxyClient<AppRouter>({
links: [httpBatchLink({url: 'http://localhost:4000/trpc'})]
});
6. Using types in UI
async function loadTodos() {
const todos = await trpc.getTodos.query(); // typed as Todo[]
return todos;
}
Because Todo is shared, changes propagate to both server and client during development, and the editor helps you when the type evolves.
7. Validation and runtime safety
tRPC integrates with Zod for input validation. Use Zod schemas at procedure boundaries to ensure runtime safety for external inputs, then derive TypeScript types from Zod if desired.
8. Deployment considerations
For production, compile the server with tsc or SWC and deploy like any Node server. Ensure the client is built with bundler of your choice. If you share types via a package, publish or use workspace linking during deployment.
9. Testing and contracts
Unit test procedure implementations and rely on type checking to catch signature mismatches between server and client. Integration tests can exercise the HTTP endpoints for end-to-end verification.
Conclusion
tRPC simplifies type flow between server and client, reducing boilerplate and making refactors safer. It’s particularly effective in monorepos or projects where sharing types is practical. Start by extracting shared DTOs and incremental adoption — you won’t need to rewrite your entire API to benefit from typed procedures.
Next step: Build a small CRUD app using the pattern above and try changing a shared type to observe how editor and tests guide the migration.
Related Topics
Linh Tran
Fullstack Engineer
Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.