tRPC

tRPC gives full-stack TypeScript apps end-to-end type safety between client and server with no schema, no code generation, and no manual API types. You define procedures on the server; the client calls them like local async functions and gets full autocomplete and compile-time checking — if you change a server procedure's input or output, the client code that uses it stops compiling immediately. The "contract" is just your TypeScript types, shared directly.

It solves a specific, common pain: in a typical REST setup, the client and server agree on a shape through documentation, manually-written types, or generated clients that drift. tRPC eliminates the gap by making the server's types be the client's types. The catch — and it's central — is that this only works when both ends are TypeScript in a shared codebase, which is exactly the Next.js/monorepo full-stack world where it thrives.

TL;DR

Quick Example

Define procedures on the server, call them type-safely on the client — no types written by hand:

Rename getUser or change its return shape on the server, and every client call breaks at compile time — the safety net REST setups lack.

Core Concepts

Procedures and Routers

The building blocks are procedures — typed functions exposed to the client:

Procedures group into routers, which nest to organize the API (trpc.user.getById, trpc.post.list). Middleware wraps procedures for cross-cutting concerns — authentication, logging — and can refine the typed context (e.g., a protectedProcedure that guarantees a logged-in user in ctx).

How the Type Safety Works (No Magic)

The mechanism is elegant and worth understanding: the client imports the router's type (import type { AppRouter }), never its runtime code. TypeScript uses that type to check every call and infer every result. At runtime, tRPC serializes calls over HTTP (a single endpoint, batched); at compile time, the shared type guarantees client and server agree. There's no generated artifact to drift — the types are the contract, and they're one source. This is why tRPC needs a shared TypeScript codebase: the client literally reaches into the server's types.

Validation with Zod

Procedure inputs are validated at runtime with a schema library (usually Zod), which does double duty: it validates incoming data and provides the TypeScript input type. One Zod schema gives you runtime safety and compile-time types together — no separate type declaration:

Client Integration

tRPC pairs with React Query/TanStack Query for the client — its React adapter gives you typed hooks (trpc.getUser.useQuery({ id })) with caching, loading states, and invalidation, combining tRPC's type safety with React Query's data-management. This is the standard full-stack setup (notably the T3 stack: Next.js + tRPC + Prisma + Tailwind).

tRPC vs REST vs GraphQL

The decision is mostly about who consumes the API:

tRPC isn't a REST/GraphQL replacement — it's the right tool for the specific (and very common) case of a TypeScript app talking to its own TypeScript backend. Many systems use tRPC internally and expose REST/GraphQL at the public edge.

Common Mistakes

Using tRPC for a Public API

tRPC's contract is TypeScript types, not a language-neutral schema — external consumers, mobile apps in other languages, and partners have nothing to build against. For anything beyond your own TS client, expose REST or GraphQL. (tRPC-OpenAPI adapters exist but fight the model.)

Expecting It Across Separate Codebases

The type safety depends on the client importing the server's types, which needs a shared codebase (or published-types package). Client and server in unrelated repos without shared types lose the entire benefit — tRPC is a monorepo/full-stack-app tool.

Skipping Input Validation

The client is typed, but requests can still arrive malformed (a non-TS caller, a bug, an attacker). Types are compile-time only — always validate inputs at runtime with Zod (or similar). tRPC's .input() is where this belongs; don't treat compile-time types as runtime guarantees.

Ignoring the Public-Boundary Question Early

Building an entire product on tRPC, then discovering you need a public API or a native mobile app, means retrofitting REST/GraphQL. Decide up front whether external, non-TS clients are in your future; if likely, plan the boundary (tRPC internal, REST/GraphQL external) from the start.

FAQ

Does tRPC replace REST and GraphQL?

No — it's for a specific case: a TypeScript client talking to its own TypeScript server, ideally in one repo. There it's often better (zero boilerplate, automatic type safety). For public APIs, non-TS clients, or many diverse consumers, REST and GraphQL remain the right tools. Think "internal full-stack glue," not "API standard replacement."

How does tRPC achieve type safety without codegen?

The client imports the server router's TypeScript type directly and uses it to type every call and result — no generated SDK, no schema file to keep in sync. Because there's a single source (the server types) that the client reads at compile time, nothing can drift. At runtime it's just HTTP; the safety is entirely in the shared types.

What do I need to use tRPC?

TypeScript on both client and server, sharing types (a monorepo or shared package), and a validation library (Zod) for inputs. It integrates naturally with Next.js and React Query. If your stack isn't TypeScript end-to-end, tRPC isn't for you.

tRPC or GraphQL?

Both give typed APIs, differently. tRPC: TS-only, zero schema/codegen, best for your own full-stack app. GraphQL: language-neutral schema, flexible client-driven queries, good for public APIs and many client types — at the cost of more setup and codegen. Choose tRPC for internal TS work, GraphQL when clients are diverse or external.

Can I add tRPC to an existing app?

Yes, incrementally — it mounts on a single HTTP endpoint and coexists with existing REST/GraphQL routes. You can move internal TS-to-TS calls onto tRPC while keeping REST for public/external needs. It's additive, not all-or-nothing.

Related Topics

References