What is tRPC?
Why typed RPC beats REST when both ends are TypeScript.
When both the server and the client are TypeScript, sending data between them as JSON-over-REST is throwing away half of what you already know. The shape of the data on each side, the parameter names, the return type, the nullability... you painstakingly re-declare all of it in two places, and pray the OpenAPI doc stays in sync. tRPCdeletes the wall. You call your server functions on the client like they were imports. Types flow automatically. There's no schema file, no codegen, no contract drift.
The one-sentence pitch
tRPC is typed RPC for TypeScript: write a function on the server, call it on the client, types are end-to-end with zero codegen.
The trivial example
A procedure is just a function. You define it on the server with a tRPC builder. The client imports a generated type (not runtime code) for the router and gets full IntelliSense.
import { initTRPC } from "@trpc/server";
import { z } from "zod";
const t = initTRPC.create();
export const appRouter = t.router({
hello: t.procedure
.input(z.object({ name: z.string() }))
.query(({ input }) => {
return { greeting: "Hello, " + input.name + "!" };
}),
});
// Export just the type; the runtime never crosses the wire.
export type AppRouter = typeof appRouter;"use client";
import { trpc } from "@/lib/trpc";
export function Greeter() {
// Argument shape is inferred. Return type is inferred. Misspell a key
// and the editor underlines it before you save.
const { data } = trpc.hello.useQuery({ name: "Ada" });
return <p>{data?.greeting}</p>;
}type AppRouter = typeof appRouter. The client imports that type. TypeScript erases it at build, so nothing from the server actually ships. The wire format is still JSON. It's the editor that gets superpowers.The value vs REST when both ends are TS
Stack tRPC against a REST API in a typical TypeScript codebase:
- No duplicated types. The same Zod schema validates the input on the server and infers the type on the client. Nothing to copy-paste, nothing to keep in sync.
- Refactor across the boundary. Rename a field on the server and every client that uses it breaks at compile time. Try doing that with REST.
- No URL design.You don't argue about whether something is
POST /users/:id/followorPUT /users/:id/relationships/follow. It's justusers.follow. - Tiny payloads. Procedures map 1:1 to your actual UI needs, so you stop fetching big resource objects when you only wanted one field.
- Built-in batching and TanStack Query. The client pipes through React Query for caching, retries, mutation state, and optimistic updates with no extra wiring.
When NOT to use tRPC
tRPC's superpower (types via import) is also its blast radius. You should reach for REST or GraphQL when:
- You need a public API.Third-party developers aren't going to import your
AppRoutertype. They want OpenAPI, versioning, and stable URLs. REST or GraphQL is the answer. - You have a polyglot client.A mobile team writing Swift or Kotlin can't consume TypeScript types. You can use tRPC inside the web app and expose a REST surface for everyone else, but tRPC alone won't serve them.
- You need long-lived stability. tRPC routers change like internal code does. A versioned REST contract is better when consumers are external and pinning matters.
- Heavy spec needs. If you specifically want OpenAPI docs, mock servers, or third-party tooling like Postman collections, REST has the ecosystem.
What "no codegen" really means
Frameworks like GraphQL Code Generator do real work: they parse your schema and produce TypeScript files. tRPC skips that because the schema isTypeScript. Your router's type signature describes every input, output, error path, and middleware. When you edit a procedure, the type updates on save. No watcher process. No generated folder to gitignore. No out-of-date types because someone forgot to re-run the script.
How it's shaped at the call site
The client object mirrors the router structure. Nested routers become nested namespaces. Queries and mutations expose hooks.
// Router shape: { post: { list, byId, create } }
trpc.post.list.useQuery();
trpc.post.byId.useQuery({ id: "abc" });
trpc.post.create.useMutation();Hovering over any of these in your editor shows the input shape, output shape, and any errors thrown by the procedure. The feedback loop becomes "type, get red squiggle, fix" instead of "type, run, see error in DevTools, fix."
Looking ahead
The next two lessons cover the missing pieces: how to structure routers, add input validation, context, and protected procedures (next lesson), and how to wire tRPC into Next.js App Router with both Server Component prefetching and client-side useQuery (lesson after that).
Quiz
Where do the types come from in tRPC?
Recap
- tRPC = typed RPC for TypeScript. Server functions become client hooks with full types.
- No codegen, no schema file. Types flow via
typeof appRouter. - Refactors across the boundary surface at compile time.
- Use it for internal apps where both ends are TS.
- Don't use it for public APIs or polyglot clients. Pair with REST when you need both.