REST vs GraphQL: an honest comparison
REST models an API as resources at URLs, manipulated with HTTP methods. GraphQL models it as one typed graph, queried through a single endpoint where the client specifies exactly the shape of data it wants. Neither is newer-is-better: REST remains the right default for most APIs, and GraphQL earns its complexity in a specific set of situations, mostly involving many clients with divergent data needs.
Both are easy to poke at from the browser with the API Tester; a GraphQL call is just a POST with a JSON body.
The same task in both styles
Fetching a user with their three latest posts, each with a comment count.
REST, typically three requests:
GET /users/42
GET /users/42/posts?limit=3
GET /posts/17/comments/count (× 3)
GraphQL, one request describing the exact shape:
{
user(id: 42) {
name
posts(limit: 3) {
title
commentCount
}
}
}
The response mirrors the query, no more, no less. This example is GraphQL's whole pitch in miniature, and it is genuinely compelling for the case shown: a client composing a view from several related resources.
What problems was GraphQL built to solve?
Overfetching. GET /users/42 returns every field the API defines, though the mobile screen needed a name and an avatar. On constrained networks, at scale, this is real waste.
Underfetching, the N+1 request problem. The view above needed one round trip per post for comment counts. Mobile apps at Facebook scale, in 2012, over slow cellular links, is precisely the environment GraphQL was invented in, and that origin explains its design.
REST teams solve these too, with sparse field parameters (?fields=name,avatar), embedding (?include=posts.commentCount), or purpose built endpoints per screen. Those work, but each is a bespoke convention; GraphQL standardizes the capability into the query language itself.
What does REST keep that GraphQL gives up?
HTTP caching for free. A REST GET /posts/17 is cacheable by the browser, by CDNs, by anything speaking HTTP, using nothing but the Cache-Control machinery the web already runs on. GraphQL sends POSTs to one URL, opaque to every HTTP cache, so caching moves into application code: client libraries with normalized stores, persisted queries, custom server layers. Apollo and Relay are substantial pieces of software largely because they rebuild caching.
Legible errors and monitoring. REST speaks in status codes: a 404 is a 404 in logs, dashboards, and proxies. GraphQL returns 200 OK for nearly everything, including failures, with errors described in the response body. Monitoring, rate limiting, and alerting all need GraphQL aware tooling to see what is actually happening.
Predictable cost. Every REST endpoint does a known amount of work. A GraphQL endpoint accepts arbitrary queries, including pathological ones five levels deep across relations. Production GraphQL needs depth limits, query cost analysis, and timeouts, an entire discipline REST sidesteps.
A smaller stack. A REST endpoint is a route and a handler. GraphQL adds a schema language, resolvers, dataloaders to avoid re-creating the N+1 problem server side, and usually a client framework.
When should you use REST, and when GraphQL?
REST fits when:
- The API is public. Third party developers understand REST instantly, and API keys plus per endpoint rate limits are well trodden.
- Payloads are resource shaped already: CRUD services, webhooks, integrations.
- HTTP caching matters: content sites, high read ratios, CDN friendly data.
- The team is small and the client count is one or two. The flexibility GraphQL sells goes unused when you control both ends and can just add the endpoint you need.
GraphQL fits when:
- Many clients with different needs share one API: iOS, Android, web, partners, each screen wanting a different slice.
- Views compose deeply relational data, and round trips hurt: feeds, dashboards, anything social graph shaped.
- Frontend teams outnumber backend teams and ship faster than endpoints can be added. The self serve quality is organizational, not just technical: frontends stop waiting on API changes.
- A schema as a typed contract, with generated types end to end, is worth real money to you.
Versioning deserves a mention: REST APIs typically version with /v2/ paths, a blunt instrument; GraphQL's convention is a single evolving schema with @deprecated fields, which works well but demands discipline about never breaking existing queries.
The overlooked middle ground
Many teams reach for GraphQL when their actual problem is smaller. Worth considering first:
- Sparse fields and includes on REST endpoints handle moderate overfetching cheaply.
- A backend for frontend, one endpoint per screen that aggregates internally, fixes round trips without a new query language.
- tRPC or similar, for TypeScript monorepos where the API is internal: typed contracts without schemas or resolvers.
GraphQL adopted for one mobile app talking to one backend is the most common regret story: all the tooling weight, none of the multi client payoff.
Deciding in one table
| Situation | Lean toward |
|---|---|
| Public API | REST |
| One frontend, one backend | REST |
| Many clients, divergent data needs | GraphQL |
| Deeply relational, composed views | GraphQL |
| Heavy read traffic, CDN caching | REST |
| Autonomous frontend teams at scale | GraphQL |
| Internal TypeScript to TypeScript | tRPC or REST |
Both are just HTTP underneath. A GraphQL query is a POST with a JSON body of the form {"query": "..."}, which you can send with the API Tester or curl and inspect like any other response. Trying both shapes against a real API for ten minutes teaches more than any comparison table.