A practical guide to choosing between GraphQL and REST based on how your clients fetch data, cache, and evolve over time.
Teams still argue about GraphQL vs REST as if one has to win. It doesn't. They solve overlapping problems with different trade-offs, and the right call depends on how your clients actually fetch data. This post walks through the core difference, what GraphQL fixes, what it costs you, and where plain REST is still the better tool.
REST organizes an API around resources, each with its own URL. You have /users/42, /users/42/orders, /products/9, and the server decides what fields come back from each one. The shape of the response is fixed by the endpoint.
GraphQL flips that. You expose a single endpoint, usually /graphql, backed by a typed schema. The client sends a query describing exactly the fields it wants, and the server returns that shape and nothing else. The schema is the contract, and it's strongly typed, so tools can validate queries before they ever run.
That single change, moving the response shape from the server to the client, is what drives every trade-off below.
The classic pain with REST is over-fetching and under-fetching. A mobile screen needs a user's name and their last three order totals. With REST you often pull a full user object, then hit a second endpoint for orders, and each order carries fields the screen never displays.
Here's the REST version of that screen:
GET /users/42
GET /users/42/orders?limit=3
// GET /users/42 returns the whole user
{ "id": 42, "name": "Ada", "email": "...", "address": {...}, "createdAt": "...", "preferences": {...} }
Two round trips, and the first response is mostly wasted bytes.
The GraphQL version is one request that names its fields:
query ScreenData {
user(id: 42) {
name
orders(last: 3) {
total
}
}
}
{ "data": { "user": { "name": "Ada", "orders": [{ "total": 39.9 }, { "total": 12.5 }, { "total": 88.0 }] } } }
One trip, only the fields you asked for. On a phone over a weak connection, that difference in payload size and round trips is real. It also helps evolving frontends. When a new screen needs a field, the client adds it to its query instead of waiting for a backend team to build or extend an endpoint. That decoupling is often the strongest reason teams adopt GraphQL.
None of this is free. The costs are just less visible on day one.
Caching: REST leans on HTTP. A GET /products/9 can be cached by a CDN, a browser, or a proxy using standard headers, and everyone downstream benefits. GraphQL requests are usually POSTs to one URL with varying bodies, so that layer of caching doesn't apply. You end up caching at the field or object level with tools like a persisted-query cache or a client store, which is more work.
N+1 queries: A query that asks for 50 posts and each post's author can trigger one database call for the posts and then 50 more for authors. You fix this with batching tools like DataLoader, but it's a footgun you have to actively defend against, and it's easy to ship a slow resolver without noticing.
Security and abuse: Because clients compose their own queries, they can compose expensive ones. A deeply nested query can walk relationships in a loop and hammer your database. You need query-depth limits, cost analysis, and timeouts as standard defenses. Rate limiting is harder too, since counting requests to one endpoint tells you little when one query can be a thousand times heavier than another.
Server complexity: You're now maintaining a schema, resolvers, and the wiring between them. For a small service, that's more moving parts than a handful of REST handlers.
REST is still the better default in several cases:
curl and any HTTP client, and doesn't ask consumers to learn a query language. That lowers the barrier for third parties.If you want a deeper checklist for either style, our API design best practices guide covers versioning, pagination, and error handling that apply no matter which you pick.
One thing worth calling out on its own is GraphQL's typed schema. Because every field has a type, you get generated client code, editor autocomplete, and validation before a query executes. The schema doubles as living documentation that can't drift from the implementation, since queries fail if they reference fields that don't exist. REST can get similar guarantees with OpenAPI, but that's a separate spec you have to keep in sync by hand or by tooling, whereas in GraphQL the schema is the API.
Most mature systems don't choose one and burn the other. A common pattern is REST for public and machine-to-machine endpoints where caching and simplicity matter, and GraphQL for the internal API that powers your web and mobile clients with their varied, changing data needs. Plenty of teams also put a GraphQL layer in front of existing REST services so frontends get flexible queries without a full rewrite. If you're comparing GraphQL to other efficient internal protocols, our gRPC vs REST post covers that axis, since gRPC often competes for the same service-to-service slot.
Ask a few concrete questions:
For a public or heavily cached API, start with REST and don't apologize for it. For an internal API feeding several fast-moving clients, especially mobile, reach for GraphQL and budget from day one for caching strategy, DataLoader-style batching, and query-depth limits. And accept that the honest answer for most large products is both, each used where it's strongest, rather than a single winner.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
A practical guide to designing REST APIs that stay predictable, easy to consume, and safe as your service grows.
A practical look at REST and GraphQL versioning, breaking changes, deprecation policy, and the pragmatic default we actually reach for.
Explore more articles in this category
A practitioner's tour of where WebAssembly earns its keep in 2026, from browser apps to edge compute, plus the places it still doesn't fit.
A practical look at why Go usually outruns Python at runtime, where Python holds its own, and how to pick per workload.
A grounded look at WebAssembly, the portable binary format that runs code at near-native speed inside a secure sandbox.
Evergreen posts worth revisiting.