tRPC vs REST: End-to-End Type Safety or a Standard API?
tRPC lets a TypeScript front end call back-end functions with full type safety and no API schema to maintain; REST is the universal, language-agnostic standard. How they differ, a tRPC example, when tRPC shines (TypeScript monorepos), when REST wins (public APIs, mobile, other languages), and where Server Actions fit.
If your front end and back end are both TypeScript, you have a choice REST alone used to make for you. tRPC lets the client call server functions directly, with types flowing end to end — change a field on the server and the front end shows a type error immediately.
REST in one paragraph
Resources at URLs, HTTP methods for actions, JSON in and out: GET /api/invoices/42, POST /api/invoices. Any language can call it, caches and proxies understand it, and tools like OpenAPI describe it. But the types on the client and server are separate — you keep them in sync by hand or with code generation. (Designing a REST API, OpenAPI vs Swagger)
tRPC in one example
// server/router.ts
import { initTRPC } from '@trpc/server'
import { z } from 'zod'
const t = initTRPC.context<{ userId: string | null }>().create()
export const appRouter = t.router({
invoice: t.router({
byId: t.procedure
.input(z.object({ id: z.string() }))
.query(({ input, ctx }) => db.invoice.findFirst({ where: { id: input.id, ownerId: ctx.userId } })),
create: t.procedure
.input(z.object({ amount: z.number().positive() }))
.mutation(({ input, ctx }) => db.invoice.create({ data: { ...input, ownerId: ctx.userId } })),
}),
})
export type AppRouter = typeof appRouter
// client
const invoice = await trpc.invoice.byId.query({ id: '42' })
// ^? fully typed from the server's return type
There's no schema file and no code generation: the client imports the router's type (not its code), and TypeScript does the rest. Input is validated with Zod at runtime. It integrates with TanStack Query for caching. (Validating input with Zod, TanStack Query vs useEffect)
Side by side
| tRPC | REST | |
|---|---|---|
| Type safety across the network | Automatic | Manual, or generated from OpenAPI |
| Client languages | TypeScript | Any |
| API contract | The TypeScript router | URLs + docs/OpenAPI |
| Public / third-party API | Poor fit | Standard |
| HTTP caching, CDN | Limited | Natural for GET |
| Refactoring | Rename and the compiler finds every use | Search and hope, or regenerate |
| Learning curve | Low if you know TS | Low |
When tRPC shines
- Full-stack TypeScript in one repository (or a monorepo), one team.
- Internal APIs that only your own front end calls.
- Fast iteration — refactors are safe and quick.
- AI-assisted development — type errors give coding agents immediate feedback when they change one side. (Why TypeScript makes AI code safer)
When REST wins
- Public APIs or partners integrating with you.
- Mobile apps in Swift/Kotlin, or back ends in Python, Go or others.
- Webhooks — providers send standard HTTP. (Handling webhooks reliably)
- Caching at the HTTP layer matters.
- Long-lived APIs that need versioning and documentation. (API versioning)
Many apps use both: tRPC (or Server Actions) for the app's own UI, REST for webhooks and the public API.
Where Server Actions fit
In Next.js, Server Actions give a similar "call a server function with types" experience for mutations, built into the framework. If you're all-in on the Next.js App Router, they cover much of what tRPC offers for forms and buttons; tRPC still adds a structured router, middleware, and works outside Next.js. (Next.js Server Actions)
And GraphQL?
GraphQL lets clients ask for exactly the fields they need across related data, with a typed schema. Great for many different clients with varied needs; more machinery than most small apps want. (GraphQL vs REST)
Security is the same either way
tRPC procedures, like REST endpoints, are callable by anyone with any input. Check authentication and authorisation in every procedure (tRPC middleware helps), and enforce ownership in the query. (IDOR explained, API authentication methods)
EasySpawn runs your full-stack TypeScript app — tRPC, REST or both — on one server with Postgres alongside, and Claude Code can lean on the types to refactor safely. See how it works or join the waitlist.
Related: Designing a REST API · GraphQL vs REST · Next.js Server Actions · OpenAPI vs Swagger
Keep reading
UUID vs Auto-Increment IDs: Which Primary Key Should You Use?
Sequential integers or UUIDs for your primary keys? The real trade-offs — size, index performance, guessability, merging data, leaking business metrics — why UUIDv7 changes the answer, Postgres 18's uuidv7(), and the common hybrid of internal IDs plus public IDs.
Session Cookies vs JWTs: Which Should Your App Use for Authentication?
Server-side sessions and JWTs both keep users logged in, with very different trade-offs. How each works, revocation and logout, where to store tokens (cookies vs localStorage), the hybrid access/refresh pattern, and a clear default for web apps.