REST vs GraphQL vs tRPC: Choosing an API Style
When designing a new service’s API, the debate — “REST or GraphQL? and apparently there’s this tRPC thing now” — happens at least once. The conclusion up front: the three are not ranked; they are tools optimized for different constraints, and the deciding axis is not technology but “who consumes this API.” This post covers what each style solves, what it takes on in exchange, and how to choose.
REST: the default, standing on resources and HTTP semantics #
REST models data as resources (URLs) manipulated by HTTP methods (GET, POST, PUT, DELETE). It has been the web API default for two decades, and the incumbency is earned.
- You get HTTP’s infrastructure for free. GET caching (CDNs, browsers), status codes, auth headers, redirects — every intermediary on the web understands REST.
- Lowest learning and tooling cost. Debuggable with a single curl, callable from any client in any language, with a mature OpenAPI ecosystem for docs and client generation.
The weakness: endpoint shapes are fixed by the server. When the data a screen needs diverges from the server’s responses, two problems appear — overfetching (receiving fields you don’t need) and underfetching (calling several endpoints in sequence for one screen). The more client types and the more complex the screens, the wider the divergence, and the API gets cluttered with screen-specific endpoints.
GraphQL: the client declares the shape it needs #
GraphQL attacks that divergence head-on. The server exposes one typed schema (a data graph), and the client declares exactly the fields it needs in a query, receiving them in a single request. Overfetching and underfetching disappear by definition, and the schema doubles as contract and documentation. The style peaks when there are multiple clients with genuinely different data needs — mobile, web, partners.
The costs taken on in exchange are just as concrete.
- Server complexity: resolver structures produce the N+1 problem by default, making batching mechanisms like DataLoader mandatory; and since clients can compose arbitrarily deep queries, defenses like complexity and depth limits become necessary.
- Loss of HTTP caching: with everything arriving as essentially one POST, CDN and browser caches no longer apply as-is; caching migrates into client libraries (Apollo and friends) and their normalized caches.
- Operational learning curve: schema design, versioning conventions, monitoring (which queries are slow) — a different operational skill set from REST.
In short, GraphQL is a trade that gives flexibility to clients by having the server buy complexity. For the trade to pay, the client-side diversity has to actually exist.
tRPC: types end to end — but only inside TypeScript #
tRPC changes the question itself: “if the server and client are TypeScript in the same repository, why do we need a schema or code generation at all?” Define procedures (functions) on the server, and the client shares their types at compile time, getting remote calls with autocompletion and type checking.
- No schema files, no codegen step. Change a server function’s signature and client code breaks right there as a type error. API mismatches caught at compile time rather than runtime — that developer experience is tRPC’s entire pitch, and its biggest strength.
- The applicability conditions are explicit: full-stack TypeScript + a monorepo (or any type-sharing structure) + internal consumers. The front-to-back channel of a Next.js full-stack app is the canonical fit.
The limits are the same conditions, seen from the back. The moment a consumer outside TypeScript appears — native mobile, a service in another language, an external partner — the type-sharing premise collapses. tRPC is not a public API tool; it is an internal communication tool for an app owned by one team.
Choosing: three questions #
| Question | Answer | Recommendation |
|---|---|---|
| Is the API exposed externally? | Yes | REST (+ OpenAPI). When you cannot control consumers, universality wins |
| Multiple clients with different data needs? | Yes | GraphQL. The server-complexity bill now has a justification |
| Internal traffic of a single full-stack TS team? | Yes | tRPC. Type safety without schemas or codegen is optimal |
A few supplements:
- When in doubt, REST is the default. It is the cheapest choice to be wrong about, and the evolution path — layering a GraphQL tier (BFF) later for specific screen compositions — stays open.
- Mixing is normal. REST for the public surface, GraphQL or tRPC for first-party clients, is not a compromise — it is using the right tool per consumer.
- Performance is not a deciding axis. All three are fast when built well and slow when built badly. When an API is slow, the diagnosis is style-independent — it is a matter of finding the bottleneck in order.
Summary #
- REST gets HTTP infrastructure and universality for free; its weakness is the over- and underfetching that comes from server-fixed response shapes.
- GraphQL erases that divergence by letting clients declare their data, in exchange for server-side complexity: N+1, query defenses, and a caching redesign. The trade pays when client diversity is real.
- tRPC delivers compile-time type safety with no schema, inside full-stack TypeScript — and the premise collapses the moment a non-TS consumer appears.
- The axis is the consumer: public = REST, diverse clients = GraphQL, single-team TS internal = tRPC. When unsure, start REST and layer later.