tRPC Explained: End-to-End Type-Safe APIs Without REST
Advanced4 minInnovialabs editorial team
In short
tRPC lets a TypeScript client call server functions directly, inferring input and output types from the server code with no schema files or code generation; it shines when one team owns both sides in a TypeScript monorepo and is the wrong choice for public or multi-language APIs.
How tRPC works
tRPC (TypeScript Remote Procedure Call) is a library for building APIs where the client calls server functions as if they were local. You define procedures on the server, group them into a router, and export only the router's type. The client imports that type and gets full autocomplete and type checking for every call.
There is no OpenAPI spec, no GraphQL schema and no code generation step. Types flow through TypeScript inference: change a field on the server, and every client call that relies on it shows a compile error immediately. Under the hood requests are still regular HTTP calls with JSON, so tRPC runs on Node.js, edge runtimes and in frameworks like Next.js.
Routers and procedures with validation
A procedure is either a query (reading data) or a mutation (changing data). Input is validated at runtime with a schema library such as Zod, and the same schema provides the TypeScript type.
// server/trpc.ts
import { initTRPC } from "@trpc/server";
import { z } from "zod";
import { db } from "./db"; // your database client
const t = initTRPC.create();
export const appRouter = t.router({
getProduct: t.procedure
.input(z.object({ id: z.string() }))
.query(({ input }) => db.product.findUnique({ where: { id: input.id } })),
createReview: t.procedure
.input(z.object({ productId: z.string(), text: z.string().min(10) }))
.mutation(({ input }) => db.review.create({ data: input })),
});
export type AppRouter = typeof appRouter;
On the client you create a typed client from AppRouter:
import type { AppRouter } from "../server/trpc";
// a client created with AppRouter knows every procedure:
const product = await trpc.getProduct.query({ id: "42" }); // typed result
Key building blocks you will use in practice:
- Context — per-request data such as the current user or database connection.
- Middleware — reusable logic, for example a
protectedProcedurethat rejects unauthenticated calls. - Nested routers —
user,product,orderrouters merged into one app router. - React Query integration — caching, loading states and invalidation for React apps.
Exact client setup differs between adapters and major versions, so follow the official documentation for your stack.
tRPC vs REST vs GraphQL
| tRPC | REST | GraphQL | |
|---|---|---|---|
| Contract | TypeScript types inferred from code | URLs and HTTP methods, optional OpenAPI | Schema with a type system |
| Client languages | TypeScript only, in practice | Any | Any |
| Code generation | Not needed | Optional (from OpenAPI) | Usually used for typed clients |
| HTTP caching | Possible for queries, less natural | Native and well understood | Harder, usually client-side |
| Flexible data fetching | Fixed per procedure | Fixed per endpoint | Client chooses fields |
| Best fit | Full-stack TypeScript apps | Public and integration APIs | Many clients with different data needs |
tRPC is not a replacement for REST or GraphQL in general; it removes a layer when both ends are written in TypeScript by the same team.
When tRPC is the wrong choice
- Public APIs for partners or third-party developers. They need stable, documented, language-neutral contracts.
- Clients in other languages: native iOS (Swift), Android (Kotlin), Python services, 1C.
- Separate repositories where the client cannot import server types without publishing packages.
- Long-lived versioned APIs — tRPC encourages changing the contract freely, which breaks old app versions still in use, such as mobile apps not yet updated.
- Heavy reliance on CDN/HTTP caching of API responses.
A common hybrid: tRPC for your own web app, plus a small REST or webhook layer for external integrations.
Common mistakes
- Skipping input validation because "TypeScript already checks it". Types disappear at runtime; validation protects the server from any client.
- Returning raw database models and leaking internal fields.
- Putting all procedures into one giant router file.
- Exporting server code instead of only the router type to the client.