← Back to Blog

TypeScript / Backend / API

Building a Type-Safe API with tRPC

End-to-end type safety without code generation — a practical walkthrough of tRPC in a Next.js monorepo.


Every API I've built in the last decade has had the same fundamental problem: the client and server disagree about the shape of the data. You change a field name on the backend, deploy, and the frontend breaks in production because nobody updated the fetch call.

tRPC eliminates this entire class of bugs by sharing TypeScript types between your client and server at build time. No code generation, no OpenAPI specs, no GraphQL schemas. Just TypeScript.

The Setup

In a Next.js monorepo, the setup is surprisingly minimal. You define your router on the server:

import { initTRPC } from '@trpc/server';
import { z } from 'zod';

const t = initTRPC.create();

export const appRouter = t.router({
  posts: t.router({
    list: t.procedure
      .input(z.object({
        limit: z.number().min(1).max(100).default(20),
        cursor: z.string().optional(),
      }))
      .query(async ({ input }) => {
        const posts = await db.post.findMany({
          take: input.limit + 1,
          cursor: input.cursor ? { id: input.cursor } : undefined,
        });

        return {
          items: posts.slice(0, input.limit),
          nextCursor: posts[input.limit]?.id,
        };
      }),
  }),
});

export type AppRouter = typeof appRouter;

And on the client, you get full autocomplete and type checking:

const { data } = trpc.posts.list.useQuery({ limit: 10 });
// data is fully typed: { items: Post[]; nextCursor?: string }

Zod for Runtime Validation

One of tRPC's best features is its integration with Zod. Every input is validated at runtime, so you get both compile-time type safety and runtime protection against malformed requests. This is the best of both worlds.

Mutations and Optimistic Updates

tRPC's mutation support works seamlessly with React Query under the hood:

const createPost = trpc.posts.create.useMutation({
  onSuccess: () => {
    utils.posts.list.invalidate();
  },
});

Optimistic updates follow the same React Query patterns you already know, but with full type safety on the update payload.

When Not to Use tRPC

tRPC is fantastic for internal APIs in a TypeScript monorepo. But if you need to expose a public API that non-TypeScript clients will consume, REST or GraphQL is still the better choice. tRPC is a tool for teams, not for ecosystems.

The Verdict

After six months of using tRPC in production, I can't imagine going back to manually typed fetch calls. The developer experience improvement is dramatic — catching API contract violations at build time instead of in production is genuinely transformative.