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
- tRPC provides type-safe client-server calls without codegen or schemas — the server's TypeScript types flow directly to the client.
- You write procedures (queries and mutations) grouped into routers; the client imports the router's type and calls procedures as typed functions.
- No REST endpoints, no GraphQL schema, no OpenAPI, no generated SDK — change the server, and mismatched client code fails to compile.
- Input validation is typically via Zod, which also supplies the input types.
- It's the darling of the TypeScript full-stack ecosystem (T3 stack, Next.js) and integrates with React Query for client caching.
- Hard constraint: both client and server must be TypeScript, ideally in one repo. For public APIs, non-TS clients, or third-party consumers, use REST or GraphQL.
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:
- Queries — read data (like GET).
- Mutations — change data (like POST/PUT/DELETE).
- Subscriptions — real-time streams (over WebSockets/SSE).
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:
- Your own TypeScript front-end, same repo/monorepo → tRPC is a superb fit; the type safety and lack of boilerplate are a genuine productivity leap.
- Public API, mobile apps, third parties, non-TS clients → tRPC doesn't fit (no schema for others to consume, TS-only) — use REST or GraphQL.
- Many diverse clients needing flexible queries → GraphQL.
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
- TypeScript — The language tRPC's entire value depends on
- REST API Design — The alternative for public/multi-language APIs
- GraphQL — Schema-based typed APIs for diverse clients
- Next.js — tRPC's most common home
- Data Fetching — React Query, tRPC's client-side partner
- Monorepos — The codebase structure tRPC assumes
- Prisma — Frequently paired (the T3 stack)