REST vs GraphQL vs tRPC — When Each Wins
TL;DR
REST is the universal default — HTTP semantics, every cache layer understands it, every client supports it. GraphQL solves over-fetching and under-fetching by letting the client declare its shape — at the cost of operational complexity (N+1 on the server, schema lifecycle, caching nuance). tRPC trades portability for end-to-end TypeScript — the client gets typed RPCs as if they were local functions, but only works in a same-monorepo TS/TS stack. The senior answer is “depends on the team shape and the client mix” — not religion.
Interview Q&A
Q: When is REST the right default?
A:
- Multiple unknown clients (mobile, third parties, partners). HTTP semantics + OpenAPI generate clients in every language.
- Heavy caching at the edge. GET responses are cacheable by every CDN, browser, reverse proxy.
- Resource-shaped domain (CRUD over
users,orders,payments). Maps cleanly to nouns + verbs. - Team familiarity. No one needs to learn anything new.
Cost: over-fetching (the endpoint returns more than the screen needs) and N round-trips when the UI needs related entities.
Q: When does GraphQL actually pay for itself?
A:
- One backend, many frontends with different shapes (web + iOS + Android, each needing different fields).
- Many-to-many entity navigation (think dashboards, social feeds, multi-entity admin views).
- Public API for unknown consumers who want to query their own shape (GitHub’s GraphQL is the textbook case).
- Teams that can operate it. GraphQL pushes complexity onto the server: dataloader (N+1), persisted queries (caching), query depth/cost limits (DoS), schema evolution.
It loses when most calls are simple CRUD with no shape variation — you’ve added a query planner for nothing.
Q: When is tRPC the right call?
A: End-to-end TypeScript monorepo where the same team owns both ends.
- Zero codegen — types flow from server to client by importing the router type.
- Refactor the server, the client breaks at compile time (the dream).
- Excellent DX with TanStack Query integration.
It loses the moment you have a non-TS client (mobile, third party, partner), a separate FE/BE repo, or a public API contract. The “type safety” is type coupling; it’s an asset inside a monorepo and a liability outside.
Q: One-line trade-off summary?
A:
| REST | GraphQL | tRPC | |
|---|---|---|---|
| Wire format | JSON over HTTP verbs | one POST endpoint, JSON | JSON over HTTP |
| Schema | OpenAPI (optional) | required, typed | TS types (no schema artifact) |
| Over-fetching | yes — endpoint shape fixed | client picks fields | client picks call args (not field shape) |
| HTTP caching | native | hard (one POST endpoint) | hard (POST) |
| N+1 risk | server controls SQL | needs dataloader discipline |
server controls SQL |
| Client codegen | OpenAPI / hand-rolled | graphql-codegen |
none — import the router type |
| Cross-language | yes | yes | no (TS only) |
| Real-time | separate (SSE/WS) | Subscriptions | Subscriptions (over WS) |
| Discoverability | OpenAPI/Swagger | introspection / GraphiQL | TS autocomplete |
Q: What problems does GraphQL cause that REST doesn’t?
A:
- N+1 on resolvers — one outer list resolver triggers N inner queries unless
dataloaderbatches. - Caching — single POST endpoint, body-dependent — CDNs can’t cache by default. Persisted queries + GET fix it but require infrastructure.
- Query cost / depth limits — an attacker (or a bad query) can request a deeply nested expansion that nukes the server. You have to enforce depth/complexity limits.
- Schema lifecycle — every additive field affects every client; breaking changes require careful deprecation.
- Authorization is per-field, not per-endpoint — easy to leak data through a field on a related type.
Q: What about file uploads, binary, streaming?
A: REST handles them natively (multipart, octet-stream, range requests, ETag). GraphQL needs multipart-spec or just punts to a separate REST endpoint — most teams do keep uploads on REST even in GraphQL apps. tRPC supports them via standard fetch.
Q: What’s a “BFF” and where does it fit?
A: Backend-For-Frontend — a thin server that aggregates upstream APIs and reshapes them for one client. Often you put GraphQL or a tRPC layer on the BFF rather than expose it to all upstream services. Solves the “many clients” problem without making every service speak GraphQL.
Q: How do these interact with caching?
A:
- REST — HTTP cache (CDN, browser, reverse proxy),
Cache-Control+ETag, plus app cache (TanStack Query / SWR / RTK Query). - GraphQL — HTTP caching is awkward (one POST). The cache lives in the client normalized store (Apollo, Relay, urql) keyed by
__typename+id. Persisted queries enable HTTP/CDN caching. - tRPC — POST by default; HTTP caching same problem. App cache via TanStack Query.
Gotchas / edge cases
- “GraphQL is faster than REST” — usually not at the network layer. It’s fewer round trips for graph-shaped reads, but each query can be slower on the server.
- GraphQL N+1 is the production-killer. Onboarding GraphQL without
dataloader(or equivalent) burns the database. - tRPC over the public internet — it’s just JSON over HTTP, so you can — but you’ve thrown away the type-safety benefit, since external clients don’t share your TS types.
- REST “RESTful purity” debates are usually a distraction. Most production REST is “JSON over HTTP with sensible URLs” and that’s fine.
- GraphQL subscriptions add a separate WebSocket transport and operational surface; for simple real-time, SSE on REST is often enough.
What a senior is expected to say
- “REST is the default — pick GraphQL or tRPC only when the trade-off is clearly worth it for this team and this client mix.”
- “GraphQL solves over-fetching and N round trips, at the cost of N+1 risk, caching complexity, and per-field auth. It needs
dataloader, persisted queries, depth/cost limits, and a schema lifecycle.” - “tRPC is amazing inside a TS monorepo and useless outside one. The type safety is type coupling — feature inside, liability outside.”
- “I keep uploads on REST even in GraphQL apps; binary doesn’t fit cleanly.”
- “The right call depends on whether one team owns both ends, whether clients are uniform, and where the cache lives.”
Cross-references
- TanStack Query — the cache layer that pairs with all three: 02_tanstack_query.md
- WebSockets and SSE for real-time: 07_websockets.md, 08_server_sent_events.md
- HTTP semantics and caching: ../../backend/12_protocols/http/03_http_semantics_and_caching.md
- Backend GraphQL: ../../backend/12_protocols/graphql/
Further reading
- GraphQL spec: https://spec.graphql.org/
- tRPC docs: https://trpc.io/docs
- OpenAPI 3.1 spec: https://spec.openapis.org/oas/v3.1.0
- “GraphQL: The hard parts” — Marc-André Giroux (talks/articles on N+1, caching, federation)