What Is tRPC? End-to-End Typesafe APIs Explained
tRPC lets TypeScript clients call server functions with full type inference and no schema or codegen step. How it works and where it fits.
tRPC is a TypeScript library for building APIs where the client calls server functions directly, with full type safety, and without writing a schema, generating client code, or maintaining a separate API contract. If your frontend and backend both live in TypeScript — a monorepo, a full-stack framework, or two packages that share types — tRPC lets you import a type from the server and call the corresponding function from the client as if it were local, with autocomplete and compile-time errors the moment the two sides drift apart.
The problem it solves
A REST or GraphQL API draws a hard boundary between client and server: you define endpoints or a schema, the client fetches from them, and keeping the two in sync is a manual (or code-generation-driven) process. Add a field on the server, forget to update the client’s types, and you find out at runtime — or worse, in production. See what a REST API is for the baseline this problem starts from.
tRPC skips the intermediate contract. The server defines a “router” of procedures — plain TypeScript functions with input validation. The client imports the router’s type (not its code) and gets a fully-typed client automatically. Change a procedure’s input or return type on the server, and TypeScript flags every mismatched call site on the client immediately, with no build step or generated file to regenerate.
How it works under the hood
A tRPC server defines procedures grouped into a router:
const appRouter = router({
getUser: publicProcedure
.input(z.object({ id: z.string() }))
.query(({ input }) => db.user.findUnique({ where: { id: input.id } })),
});
export type AppRouter = typeof appRouter;
The client imports only the AppRouter type and creates a proxy client from it:
const user = await trpc.getUser.query({ id: "123" });
Under the hood, that call still goes over HTTP — tRPC serializes the input, sends a request, and deserializes the response, typically using JSON with a serializer like superjson to handle dates and other non-JSON-native values. The type safety is a compile-time illusion built on TypeScript’s structural typing: nothing is generated, no schema file exists, and the “API contract” is simply whatever the server’s types say it is. Input validation at runtime is handled by a schema library — usually Zod — since TypeScript types disappear at compile time and the server still needs to validate what actually arrives over the wire.
tRPC vs REST and GraphQL
The tradeoff is coupling. A REST API can be consumed by any client in any language, versioned independently, and cached by generic HTTP infrastructure. tRPC’s type safety depends on the client having direct access to the server’s TypeScript types — practically, that means a monorepo or a published types package, and a client that is also written in TypeScript. It is not a general-purpose API format; it’s a tool for teams that own both ends of the connection and want to eliminate the drift between them.
GraphQL solves a related problem — flexible, typed queries — but does it with a schema, a query language, and a resolver layer, which is considerably more infrastructure for a small full-stack app. REST vs GraphQL covers that tradeoff in more depth. tRPC is closer to calling a function than to querying an API: there’s no query language to learn, and the “schema” is just your existing TypeScript generics and types, inferred rather than declared separately.
When it’s the right fit
tRPC fits naturally into a full-stack TypeScript app — a Next.js project with API routes, a monorepo with shared packages, or any setup where the same team and the same repository own both client and server. It removes an entire category of bugs (mismatched request/response shapes) with very little ceremony, and pairs well with React Query for caching and state management on the client.
It’s a poor fit when the API needs to serve non-TypeScript clients, be publicly documented as a stable contract (an external SDK team, third-party integrators), or version independently from the client — those are the scenarios REST’s looser coupling and language-agnostic nature actually pay for themselves. If you find yourself hand-writing a stable, versioned public contract, tools like JSON Schema or a plain REST API are the better fit than tRPC’s tight, same-repo coupling.
The takeaway
tRPC trades API generality for elimination of the client-server type gap: no schema to maintain, no codegen step, and compiler errors the instant client and server disagree. That’s a large win inside a single TypeScript codebase or monorepo, and a poor fit the moment an API needs to serve clients outside that boundary. Reach for it when you own both ends of the wire and want the compiler to keep them honest; reach for REST or GraphQL when the API is a public or cross-language contract.
Tagged
Keep reading
Chisato · · 4 min read gRPC vs REST: Choosing an API Style
gRPC uses binary Protocol Buffers over HTTP/2 for fast, typed service calls; REST uses JSON over HTTP for accessible, resource-based APIs. How to pick.
Takina · · 4 min read REST vs GraphQL: Choosing an API Style
REST exposes fixed endpoints per resource; GraphQL lets clients query exactly the fields they need through one endpoint. How to choose between them.
The Lycoris Team · · 5 min read Thundering Herd Problem (Cache Stampede) Explained
The thundering herd problem hits when a cached value expires and every waiting request floods the backend at once. Here's why it happens and how to stop it.