Chapter 22 · REST vs GraphQL#
The big idea#
Both are ways to build a web API. They differ in who decides the shape of the response:
REST: the server decides. Each URL returns a fixed shape.
GraphQL: the client decides. It describes exactly which fields it wants.
Everyday example: a fixed-price thali versus an a-la-carte menu. A thali (REST) gives you the same set of dishes whether you are hungry or not. A-la-carte (GraphQL) lets you ask for just the two curries you want, and everything arrives on one tray.
REST: resources and verbs#
Model the world as resources with URLs and use HTTP methods on them:
GET /books list books
POST /books create a book
GET /books/42 read book 42
PUT /books/42 replace book 42
PATCH /books/42 change some fields
DELETE /books/42 delete book 42
GET /books/42/reviews reviews of book 42 (a nested resource)
Strengths: simple, uses HTTP properly (caching, status codes), easy to debug with curl, understood by
everyone, CDN-friendly for GET requests.
GraphQL: ask for exactly what you need#
One endpoint, usually POST /graphql. The client sends a query shaped like the data it wants:
query {
book(id: 42) {
title
author { name }
reviews(limit: 2) { rating text }
}
}
Response (exactly those fields, nothing more):
{"data": {"book": {"title": "Dune", "author": {"name": "Frank Herbert"},
"reviews": [{"rating": 5, "text": "Classic"}, {"rating": 4, "text": "Dense"}]}}}
A GraphQL server has a schema (types and fields) and resolvers (functions that fetch each field). Besides queries (read) it supports mutations (write) and subscriptions (live updates).
The problem GraphQL solves: over- and under-fetching#
Over-fetching: the endpoint returns 40 fields; the screen needs 3. Wastes bandwidth.
Under-fetching: one endpoint is not enough, so the client makes several round trips (
/books/42, then/authors/7, then/books/42/reviews). On a slow mobile network each trip hurts.
# The same book screen, REST vs GraphQL, counting round trips and bytes
BOOK = {"id": 42, "title": "Dune", "author_id": 7, "isbn": "978...", "pages": 412, "language": "en",
"publisher": "Chilton", "year": 1965, "price": 499, "stock": 18}
AUTHOR = {"id": 7, "name": "Frank Herbert", "born": 1920, "bio": "x" * 400}
REVIEWS = [{"rating": 5, "text": "Classic"}, {"rating": 4, "text": "Dense"}, {"rating": 3, "text": "Slow"}]
import json
def size(obj):
return len(json.dumps(obj))
# REST: three calls, each returns the full resource
rest_bytes = size(BOOK) + size(AUTHOR) + size(REVIEWS)
print(f"REST : 3 round trips, {rest_bytes} bytes")
# GraphQL: one call, only the requested fields
needed = {"book": {"title": BOOK["title"], "author": {"name": AUTHOR["name"]},
"reviews": REVIEWS[:2]}}
print(f"GraphQL: 1 round trip , {size(needed)} bytes")
Output:
REST : 3 round trips, 714 bytes
GraphQL: 1 round trip , 143 bytes
On a 150 ms mobile connection, two saved round trips are 300 ms the user does not wait.
Side by side#
REST |
GraphQL |
|
|---|---|---|
Endpoints |
Many ( |
One ( |
Response shape |
Fixed by the server |
Chosen by the client |
Round trips for related data |
Often several |
Usually one |
HTTP caching, CDN |
Easy (GET + URL) |
Hard (POST, every query differs) |
Error handling |
HTTP status codes |
Always |
Versioning |
|
Evolve the schema; deprecate fields |
File upload, streaming |
Natural |
Awkward |
Learning curve |
Low |
Medium |
Main risk |
Over/under-fetching |
Expensive queries (deeply nested), N+1 resolver calls |
The N+1 problem and query cost#
If a query asks for 50 books and each book’s author, a naive resolver runs 1 query for books + 50 queries for authors. Fix with batching (DataLoader: collect all author ids, fetch once). Also protect the server: limit query depth, complexity and use persisted queries.
When to choose which#
Situation |
Choose |
|---|---|
Public API for third parties |
REST: simple, cacheable, universal |
Simple CRUD service |
REST |
Many client types (web, iOS, Android) with different data needs |
GraphQL in front of your services |
Product team changes the UI weekly |
GraphQL (no new endpoint per screen) |
Heavily cached read traffic (CDN) |
REST |
Internal, high-volume service calls |
gRPC (see REST vs RPC) |
A common real-world setup: REST or gRPC between services; GraphQL as a gateway for the front-ends.
Common mistakes#
Warning
Choosing GraphQL “because it is modern” for a simple CRUD API: you gain a caching and security problem.
Exposing GraphQL with no depth or complexity limits (one query can take down the server).
Returning
200 OKfor every REST error. REST has status codes; use them.Forgetting the N+1 problem in resolvers.
Try it yourself#
A home screen needs the user name, 3 latest orders and the unread notification count. Compare the REST and GraphQL approaches.
Why is it harder to put a CDN in front of GraphQL than in front of REST?
What is the N+1 problem and how do you fix it?
Answers
REST: three calls (
/me,/orders?limit=3,/notifications/unread-count) or one custom endpoint. GraphQL: one query naming those fields.A CDN caches by URL. GraphQL sends different queries to the same URL via POST, so there is nothing stable to cache (unless you use persisted queries over GET).
A list query triggers one extra query per item. Fix with batching (e.g. DataLoader) or a join so related data is fetched in one or two queries.
Next
One door for all those APIs: the API gateway.