frontend / apis data fetching / 01_rest_graphql_trpc_comparison.md

REST vs GraphQL vs tRPC — When Each Wins

5 min read source

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 dataloader batches.
  • 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

Further reading