REST vs GraphQL
Short answer: REST for most APIs, GraphQL when clients have genuinely divergent data needs. REST is simpler, cacheable by default at every layer, and understood by every tool and engineer. GraphQL earns its complexity when you have multiple client types wanting different shapes of the same data, deeply nested relationships, or many teams contributing to one API surface.
The framing that helps most: GraphQL solves a client-coordination problem, not a technical one. If one team owns both the API and its single consumer, REST plus a well-designed endpoint is almost always less total work. If a web app, an iOS app, an Android app, and three partner integrations all want different subsets of the same graph, GraphQL stops you from either shipping twelve endpoints or over-fetching on every one.
Worth naming up front: for internal service-to-service calls, neither may be the right answer — gRPC usually is. And for a TypeScript monorepo with one client, tRPC gives you end-to-end type safety with far less machinery than GraphQL.
TL;DR
- REST by default. Simpler, HTTP-cacheable, universally understood, better tooling.
- GraphQL when multiple clients need different shapes of the same data, or many teams share one API.
- GraphQL's real win is eliminating client-server round trips for nested data, not "no over-fetching."
- REST's real win is HTTP caching — CDN, browser, and proxy caching work for free.
- GraphQL moves complexity to the server: N+1 batching, query cost limits, auth per field.
- REST versioning is explicit (
/v2); GraphQL evolves by adding fields and deprecating, not versioning. - gRPC for internal RPC; tRPC for a TypeScript monorepo with one client.
- You can use both — GraphQL for the client-facing graph, REST for webhooks, uploads, and public APIs.
Head-to-Head
The Problems GraphQL Actually Solves
Round-trip waterfalls
This is the strongest argument, and it's about latency rather than payload size.
A REST API can solve this with a purpose-built composite endpoint. The point is that GraphQL solves it generally, without the server anticipating each combination — which is exactly the coordination problem.
Divergent client needs
In REST this becomes ?fields=, ?include=, ?expand= parameters that gradually reimplement a query language badly — or separate endpoints per client, which is the coordination cost GraphQL removes.
A schema that can't go stale
The schema is introspectable, machine-readable, and always current because it is the implementation. OpenAPI achieves the same thing when maintained, and is frequently hand-written and drifted. Codegen from a GraphQL schema gives clients types that cannot be wrong.
The Problems GraphQL Creates
HTTP caching stops working
This is GraphQL's most consequential cost and the one most often underestimated.
You get client-side normalized caching (Apollo, urql, Relay), which is genuinely good for a single-page app. You lose CDN and browser HTTP caching, which is free and enormously effective. Mitigations — persisted queries over GET, automatic persisted queries, edge caching by query hash — work, and are additional machinery reimplementing what REST had by default.
For a content-heavy public site where CDN caching is the primary performance strategy, this alone can rule GraphQL out.
N+1 queries, now on the server
GraphQL moves the round-trip problem from the client to your database. Every relationship resolver needs batching, and the loaders must be per request — a module-scoped DataLoader caches across users and leaks data between requests. This is table stakes, and it's work REST didn't require.
Query cost becomes an attack surface
REST endpoints have bounded cost by construction. GraphQL requires depth limiting, query complexity analysis, pagination enforcement, and ideally persisted queries allowing only pre-registered operations. None of this is optional on a public GraphQL API, and all of it is work.
Authorization gets granular
REST authorizes per endpoint, which is coarse and easy to audit. GraphQL authorizes per field, which is precise and easy to get wrong — a field exposed through an unexpected traversal path is a recurring vulnerability class.
Errors don't use HTTP
Every monitoring tool, load balancer, CDN, and alerting rule that keys on HTTP status sees success. You need GraphQL-aware observability, and "our error rate is 0%" becomes something your dashboards report while users see failures.
The Decision Framework
Using both
This is what most large systems actually do. GraphQL is bad at file uploads, webhook receipt, and OAuth flows; REST handles those naturally. Running both is normal and not a design failure.
Best Practices
If you choose REST, design endpoints for your screens
The over-fetching complaint is usually a symptom of endpoints designed around database tables rather than around what a client actually renders. A /api/dashboard endpoint returning exactly what the dashboard needs eliminates the waterfall without a query language. Sparse fieldsets (?fields=id,name) handle the rest.
If you choose GraphQL, batch every relationship resolver
DataLoader, constructed per request in the context factory. This is not optional — an unbatched resolver turns one client query into hundreds of database queries, which is worse than the REST waterfall you were fixing.
Enforce query cost limits before going public
Depth limiting, complexity scoring, mandatory pagination arguments, and a timeout. For a first-party client, persisted queries — where the server only executes pre-registered operations — eliminate the entire attack surface and enable GET-based CDN caching at the same time.
Don't version a GraphQL schema
Add fields, deprecate old ones with @deprecated(reason:), and remove them after observing that no client uses them. Schema checks against recorded production operations are what make this safe. A /graphql/v2 defeats the purpose.
Use codegen on either side
openapi-typescript or oazapfts for REST; graphql-codegen for GraphQL. Hand-written client types drift from the server, silently. This is one of the largest practical wins available on either stack.
Return proper HTTP semantics from REST
Correct status codes, Cache-Control, ETag, Location on creation, Retry-After on 429. Most of REST's advantages come from actually using HTTP rather than tunnelling RPC through POST.
Instrument GraphQL per operation, not per route
Name every operation (query UserFeed { … }), and trace, log, and alert on the operation name. Without this, all your metrics collapse into one /graphql route and you cannot tell which query is slow.
Don't adopt GraphQL for one client you control
The coordination benefit is zero, and you still pay for resolvers, batching, cost limits, field-level auth, and the loss of HTTP caching. This is the most common way teams end up regretting GraphQL.
Common Mistakes
Adopting GraphQL for a single client
Unbatched GraphQL resolvers
A module-scoped DataLoader
A public GraphQL endpoint with no cost limits
REST endpoints shaped like database tables
Tunnelling RPC through POST and calling it REST
FAQ
Is GraphQL replacing REST?
No. Adoption grew substantially and then settled into a niche it fits well: first-party client graphs with divergent consumers, and multi-team API surfaces via federation. REST remains the default for public APIs, webhooks, and the large majority of services — and several prominent companies have publicly moved off GraphQL back to REST after finding the server-side complexity exceeded the client benefit. Both are stable, mature choices.
Doesn't REST over-fetch?
Often, and it's usually less costly than assumed. An extra 2 KB of JSON on a gzipped response is negligible compared to a round trip on a mobile network. The problem worth solving is under-fetching — the sequential waterfall — and REST solves that with composite endpoints designed around screens. Over-fetching is the argument people lead with; latency is the one that actually matters.
How do I cache GraphQL?
Client-side with a normalized cache (Apollo Client, urql, Relay), which works well for SPAs. Server-side with per-resolver caching and DataLoader's per-request memoization. At the edge, with automatic persisted queries sent as GET requests keyed by query hash, which restores CDN cacheability. It's several mechanisms doing what Cache-Control does for free in REST.
What about gRPC?
The right answer for internal service-to-service communication: binary Protobuf, generated clients in every language, HTTP/2 multiplexing, bidirectional streaming, and substantially better performance than JSON over HTTP. Its weakness is browsers, which need grpc-web and a proxy. The common architecture is gRPC internally, REST or GraphQL at the edge. See gRPC.
And tRPC?
Excellent in its specific niche: a TypeScript monorepo where the same team owns server and client. You get end-to-end type inference with no schema, no codegen, and no build step — types flow directly from your server functions. It doesn't cross language boundaries and isn't suited to a public API, which is the tradeoff. For a Next.js app with a TypeScript backend it's frequently the best answer and beats both REST and GraphQL on developer experience. See tRPC.
Can I add GraphQL on top of existing REST APIs?
Yes, and it's a common migration path — a GraphQL layer whose resolvers call your existing REST services. It gives clients the unified graph immediately. The caveat is that it inherits the REST layer's latency and can make the N+1 problem worse, since each resolver becomes an HTTP call rather than a database query. Aggressive batching and caching in that layer are essential.
Which is better for a public API?
REST, in nearly all cases. Every language has an HTTP client, every developer already understands it, curl works, HTTP caching works, and you control the cost of every endpoint. A public GraphQL API means third parties can write arbitrarily expensive queries, which is a rate-limiting problem with no clean solution — GitHub offers both, and their GraphQL API uses a point-based rate limit precisely because of this.
Related Topics
- REST API Design — In depth
- GraphQL — In depth
- GraphQL Federation — When many teams share one graph
- gRPC — The right choice for internal RPC
- tRPC — TypeScript monorepos with one client
- API Architecture — The full landscape of choices
- API Design — Principles that apply to both
- API Versioning — Where the two differ most
- Caching — REST's structural advantage
- Rate Limiting — Query cost analysis for GraphQL
- API Documentation — OpenAPI vs schema introspection