GraphQL vs REST
REST has served the web well for two decades, and most teams adopting GraphQL are migrating from it. So the honest question is not “which is better” but “what does each one optimize for, and which trade-offs am I choosing?” This lesson lays the two side by side.
Many endpoints versus one graph
Section titled “Many endpoints versus one graph”A REST API models resources as URLs. To build a profile page you might call three:
GET /users/42GET /users/42/postsGET /posts/99/commentsThe equivalent GraphQL request is one query to one endpoint, walking from the user to their posts to each post’s comments in a single round trip:
{ user(id: "42") { name posts { title comments { body } } }}flowchart TB
subgraph REST["REST — three round trips"]
direction LR
R1["GET /users/42"] --> R2["GET /users/42/posts"] --> R3["GET /posts/99/comments"]
end
subgraph GQL["GraphQL — one round trip"]
direction LR
G1["POST /graphql"] --> G2["user -> posts -> comments"]
end Over- and under-fetching
Section titled “Over- and under-fetching”This is where the difference is felt day to day.
- With REST,
GET /users/42returns a fixed payload. If you only need the name you still receive the whole record (over-fetching); if you need related posts you make another call (under-fetching). - With GraphQL, you list exactly the fields you want, across related types, and get them in one response. Neither problem arises by default.
A side-by-side comparison
Section titled “A side-by-side comparison”| Concern | REST | GraphQL |
|---|---|---|
| Endpoints | Many, one per resource | One graph endpoint |
| Response shape | Fixed by the server | Chosen by the client |
| Over/under-fetching | Common | Avoided by design |
| Versioning | Often /v1, /v2 URLs | Evolve the schema; deprecate fields |
| HTTP caching | Easy — URLs cache well | Harder — needs app-level or persisted-query caching |
| Discoverability | OpenAPI/docs, by convention | Introspection built into the schema |
| Learning curve | Familiar to most teams | Schema, resolvers, and N+1 to learn |
Versioning
Section titled “Versioning”REST APIs commonly version through the URL — /v1/users, then /v2/users. GraphQL leans on continuous evolution instead: add new fields freely (they are opt-in, so old clients are unaffected), and mark old fields with @deprecated to guide clients off them over time. There is rarely a hard version bump.
The caching trade-off
Section titled “The caching trade-off”This is GraphQL’s most real cost. REST’s GET /posts/99 is a stable URL that browsers, CDNs, and proxies cache for free. GraphQL queries usually arrive as POST requests with the query in the body, so that layer of HTTP caching does not apply out of the box. Teams recover it with techniques like persisted queries (sending a stable hash instead of the full query text) and normalized client caches (such as those in Apollo Client or urql). It is solvable, but it is not free.
See the round-trip difference
Section titled “See the round-trip difference”The runner below executes a single GraphQL query that, in a REST world, would have needed three separate calls. Notice how one request returns a nested, ready-to-render structure.
When each one fits
Section titled “When each one fits”Reach for REST when your API is resource-shaped and cache-heavy (think public, read-mostly content served through a CDN), when consumers are simple, or when the team already ships REST well and the extra moving parts would not pay off.
Reach for GraphQL when many different clients need different slices of the same data, when views aggregate several related resources, when product teams iterate fast and want to add fields without coordinating endpoint releases, or when strong typing and introspection across the API surface bring real leverage.
Common mistakes
Section titled “Common mistakes”Reaching for GraphQL because of hype, not a problem
Symptoms:
- an API has 5 clear endpoints, but the team switches to GraphQL because it is “modern”
- the team spends time setting up GraphQL instead of shipping features
Assuming GraphQL is always faster than REST
Symptoms:
- no DataLoader — N+1 makes GraphQL much slower than REST
- no query depth limit — a client sends a deeply nested query and overloads the server
💡 Real-world examples
Stripe:
- Stripe’s main public API is still REST today, not GraphQL (Stripe API reference)
- proof that REST still fits a public API that needs HTTP caching, easy-to-reason-about versioning, and broad client-library support
Airbnb:
- uses Apollo Federation for its internal GraphQL platform across teams (Airbnb Engineering: Reconciling GraphQL and Thrift)