04 · GraphQL vs REST¶
REST models an API as a set of resources reached by URL. GraphQL models it as a single endpoint with a typed schema the client queries — the client asks for exactly the fields it needs, in one round trip, no more.
The core difference, side by side¶
REST — three requests to render a profile page with recent orders:
curl https://api.example.com/v1/users/42
curl https://api.example.com/v1/users/42/orders?limit=5
curl https://api.example.com/v1/users/42/orders/91/items
GraphQL — one request, one response, exactly the shape asked for:
curl -X POST https://api.example.com/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "{ user(id: 42) { name orders(limit: 5) { id total items { name } } } }"
}'
{
"data": {
"user": {
"name": "Ada",
"orders": [
{ "id": 91, "total": 42.50, "items": [ { "name": "Widget" } ] }
]
}
}
}
Why teams reach for GraphQL¶
- Over-fetching — a REST
/users/42might return 30 fields when the client needs 3; GraphQL returns only the requested fields. - Under-fetching / N+1 round trips — REST often needs a chain of calls (user → orders → items); GraphQL does it in one.
- Client-driven shape — different clients (mobile vs. web) can ask for different fields from the same schema without new backend endpoints.
Why REST still wins in most cases¶
- HTTP caching — REST's
GET /v1/users/42is cacheable by URL with standardCache-Control/ETag(module 7, Level 2). GraphQL's singlePOST /graphqlendpoint defeats HTTP caches entirely; caching has to be reimplemented at the application layer (e.g. persisted queries + a normalized client cache). - Simplicity — REST's status codes and resource model are understood by every HTTP tool (curl, browsers, proxies, CDNs) with no extra tooling. GraphQL needs a client library to be pleasant to use.
- File uploads, webhooks, streaming — all more natural as plain HTTP/REST than as GraphQL operations.
- Rate limiting and cost control — a REST endpoint has a predictable cost. A single GraphQL query can ask for deeply nested data that's expensive to compute, so servers need query complexity analysis to avoid abuse.
What a GraphQL schema looks like¶
type User {
id: ID!
name: String!
orders(limit: Int = 20): [Order!]!
}
type Order {
id: ID!
total: Float!
items: [Item!]!
}
type Query {
user(id: ID!): User
}
The schema is the contract — analogous to an OpenAPI spec (module 7, Level 1), but the client, not the server, decides the response shape at request time.
Errors in GraphQL: always HTTP 200¶
A key gotcha: GraphQL responses are almost always 200 OK, even on
error — the error lives inside the JSON body:
{
"data": { "user": null },
"errors": [
{ "message": "User 42 not found", "path": ["user"], "extensions": { "code": "NOT_FOUND" } }
]
}
This trips up REST-trained tooling that checks status codes to detect
failure; GraphQL clients must always inspect errors.
Worked example: choosing for a real product¶
A team is building a public API for a project-management tool with many different frontends (web, mobile, third-party integrations) that each need different slices of deeply nested data (projects → tasks → comments → attachments).
- If most consumers are internal frontends with fast-changing data needs → GraphQL reduces backend churn from "add another endpoint for this screen."
- If the API is also meant to be a stable, cacheable, publicly documented product other companies integrate against long-term → REST's URL-addressable resources and HTTP caching win, and many teams end up shipping both: REST for the stable public surface, GraphQL for an internal BFF layer.
How It Actually Works¶
GraphQL and REST both travel over HTTP, but GraphQL replaces "one URL per resource shape" with a single endpoint and a query language the server parses and executes at request time — a fundamentally different mechanism underneath.
A GraphQL request is a POST /graphql with a query string as the body:
The server doesn't route this to a matching URL pattern — it parses the
query into an abstract syntax tree, then walks that tree, calling a
resolver function for each field: a book resolver fetches the book
row, then an author resolver (nested under it) fetches the related
author row, each resolver typically making its own database call. This is
exactly why naive GraphQL servers suffer the "N+1 query problem" — fetching
20 books, each with an author sub-field, triggers 1 query for books plus
20 separate queries for authors unless the server batches them (typically
via a "DataLoader" that collects all pending author IDs within one tick
of the event loop and issues one WHERE id IN (...) query instead of 20).
REST's equivalent request (GET /books/17?include=author) is matched
against a fixed route pattern and runs one pre-written handler function —
no query parsing or dynamic resolver graph involved, which is why REST
responses have a fixed, predictable shape per endpoint while GraphQL
responses shape-match whatever fields the client asked for in that
specific query string.
Exercise¶
- Explain why a CDN can cache a REST
GETresponse but generally cannot cache a GraphQLPOST /graphqlresponse without extra work. - A GraphQL query returns
"data": {"user": null}and anerrorsarray, with HTTP status200. Why can't a naive REST-style client that only checks the status code detect this failure? - Design a GraphQL query for a blog: fetch a post's title and the first 3 comments' author names. Then show what that would look like as REST endpoint calls.
- What is query-complexity analysis, and why does a GraphQL server need it in a way a REST API generally doesn't?