The Home Screen That Wanted Four Requests
The Home Screen That Wanted Four Requests
A storefront app opens its home screen. The layout needs four things: a featured carousel (image, title, price), a cart badge with an item count, the signed-in user's first name, and three recommendations.
The frontend wires a waterfall of REST calls:
| Call | Endpoint | What comes back | What the screen renders |
|---|---|---|---|
| 1 | GET /products/featured |
40 products, 30 fields each | 4 fields of 4 |
| 2 | GET /cart |
line items, addresses, totals, tax | itemCount only |
| 3 | GET /users/me |
profile, addresses, preferences | firstName only |
| 4 | GET /recommendations |
20 products, 30 fields each | 4 fields of 4 |
Two separate wastes appear here.
Over-fetching is response data the client never reads. The product payloads are mostly bytes that are parsed, held in memory, and dropped. On a mid-range phone that means extra JSON parsing, larger GC pressure, and more radio time for data nobody renders.
Under-fetching is the opposite failure: no endpoint matches the screen, so the client issues four requests and waits out four round trips. Latency multiplies with the number of calls, and the four responses can even arrive out of order.
GraphQL attacks both at once. One endpoint, one request, and the selection set is the UI contract:
query HomeScreen {
featuredProducts(limit: 6) {
id
title
priceCents
imageUrl
}
cart {
itemCount
}
me {
firstName
}
recommendations(limit: 3) {
id
title
priceCents
imageUrl
}
}
The response mirrors the query shape, so the client does no pruning and the server does not ship the 30 unused product fields.
One honest limit: GraphQL removes bytes and round trips, not computation. The server still walks every field you asked for, so a greedy query can cost more than four REST calls did. That cost is controlled later with depth limits and budgets, covered in query depth limits.
The Cache You Give Up and the One You Rebuild
The Cache You Give Up and the One You Rebuild
GET /products/featured is cacheable for free. The URL names the resource, Cache-Control and ETag set the policy, and the browser, the CDN, and every proxy in between can store the exact bytes without understanding anything about your API. This is REST's quiet advantage, and it is the single most common reason teams talk themselves out of GraphQL.
A GraphQL request is almost always POST /graphql, and HTTP caches do not cache POST bodies. The cache key is built from method and URL; the operation lives in the request body, so two different queries to the same endpoint are indistinguishable to an intermediary. GraphQL therefore needs deliberate caching design at one of three layers:
- GET with a query string.
GET /graphql?query=query%7B...%7Dis cacheable like any URL, but URL length limits (proxies often cap around 2 KB) restrict it to short queries and it produces URLs no human wants to read. - Persisted queries. The client sends a hash instead of the document; the CDN keys on the hash and serves cached responses without ever seeing the text. See persisted queries.
- Entity-level response caching. The server caches per entity (
Product:123) and assembles the response from cached pieces, so the cache survives changes in which fields a client requests. See response caching strategy.
Client libraries add a fourth layer of their own: a normalized cache keyed on __typename plus id, so the same product fetched by two different queries is stored and rendered once. That is why an id field on every object type is treated as a convention rather than a preference.
| Concern | REST | GraphQL |
|---|---|---|
| Cache key | method + URL | must be designed (hash, entity, client cache) |
| Payload shape | server decides | client decides per request |
| Breaking change | new URL path | new schema version only after removal |
| Error reporting | HTTP status per call | status plus errors array per response |
| Round trips per screen | one per resource | one per screen |
The trade is real: you buy exact payloads and fewer requests, and you pay for caching infrastructure you would otherwise get from a plain HTTP cache.
Versionless Does Not Mean Effortless
Versionless Does Not Mean Effortless
REST teams version by URL. When GET /v1/products changes shape incompatibly, you ship /v2/products, run both paths, duplicate CDN entries, and wait months for old clients to migrate. Every version doubles the surface you test.
GraphQL takes a different route: the endpoint never changes, so the schema must evolve without breaking anyone. The safe moves are additive. Adding a nullable field changes nothing for existing queries. Changing a field's type, removing a field, or tightening nullability breaks queries that already exist in the wild, so those changes go through a cycle instead:
- Mark the old field
@deprecated(reason: "use priceCents"), which keeps it resolvable while surfacing a warning in tooling. - Ship the replacement field and let clients migrate query by query.
- Remove the old field only after telemetry shows zero traffic.
That cycle is only trustworthy if something enforces it. Schema checks in CI compare the proposed schema against the current one and block merges that break existing operations, see schema checks in CI and schema evolution rules. Without that gate, "we just add fields" quietly turns into "someone removed one last Tuesday."
When REST is still the right answer
- The API is a clean resource CRUD surface and responses should be cacheable at the CDN by URL.
- The work is file upload, download, or webhook delivery, where a request body of raw bytes beats a JSON envelope.
- There is one client, one team, and the schema would be ceremony rather than leverage.
- Server-to-server traffic where the consumer already shares your code and needs no discoverability.
Interviewers probe exactly here. "When would you not use GraphQL?" earns a real answer when you name a concrete cost: caching-by-URL you would have to rebuild, resolver composition you would have to staff, or a governance process you would have to run. A framework that fits everywhere fits nowhere in particular.
Practice Problems
The home screen needs: 6 featured products (id, title, priceCents, imageUrl), the cart item count, the current user first name, and 3 recommendations with the same four product fields. Today the client calls GET /products/featured, GET /cart, GET /users/me, and GET /recommendations in sequence. Write the single GraphQL document that replaces the whole waterfall, assuming the schema exposes featuredProducts(limit: Int), cart, me, and recommendations(limit: Int).
Example:
Input: REST: 4 sequential calls, each returning full product or user records
Output: query HomeScreen { featuredProducts(limit: 6) { id title priceCents imageUrl } cart { itemCount } me { firstName } recommendations(limit: 3) { id title priceCents imageUrl } }
One request replaces four round trips, and each product selection asks for exactly the four fields the cards render instead of the full 30-field record.
Optimal Solution — O(1) network round trips time, O(f) fields selected space
Mirror the layout tree in the selection set: one top-level field per UI region, leaf fields at the leaves. Round trips drop from 4 to 1 and payload size follows the fields actually selected.
query HomeScreen {
featuredProducts(limit: 6) {
id
title
priceCents
imageUrl
}
cart {
itemCount
}
me {
firstName
}
recommendations(limit: 3) {
id
title
priceCents
imageUrl
}
} Edge Cases:
- A region that is conditionally rendered can be skipped with @include(if:) instead of sending a second request
- If one top-level field fails, the response still carries partial data for the others
Quiz
1. A product card needs id, title, price, and image. GET /products returns 40 fields for every product and the app renders four of them. Which problem is this?
2. Why does a typical POST /graphql response miss the CDN cache that GET /products/featured gets for free?
3. Which schema change can ship without a new URL version because it cannot break an existing query?
4. A public, read-heavy API must let any proxy cache responses by URL. Which statement is correct?
Flashcards
Question
Over-fetching vs under-fetching
Click to reveal answer
Answer
Over-fetching: response contains fields nobody reads (wasted bytes). Under-fetching: one response is not enough, so the client issues more requests (wasted round trips).
Question
Why a POST /graphql response is not cached by default
Click to reveal answer
Answer
HTTP cache keys use method + URL; the operation is in the body, so identical URLs hide different queries. Fix: query-string GET, persisted-query hashes, or entity-level caching.
Question
How GraphQL avoids /v2 URL versioning
Click to reveal answer
Answer
Evolve additively: add nullable fields, deprecate old ones with @deprecated, remove only after traffic shows migration. Enforced by schema checks in CI.
Question
What GraphQL does NOT reduce
Click to reveal answer
Answer
Server-side work. It cuts payload bytes and round trips; a deep query still costs resolver work, so depth and cost limits are required.
Question
When to keep REST
Click to reveal answer
Answer
Clean resource CRUD that must cache by URL, file transfer and webhooks, one-client APIs, or teams with no one to own schema governance.
Revision Notes
Key Takeaways
- 1. REST fixes the payload shape in the server; GraphQL moves it into the client selection set, which removes over-fetching and under-fetching in one request
- 2. Caching by URL is free in REST and must be rebuilt in GraphQL through persisted queries, GET query strings, or entity-level caches
- 3. Versionless APIs depend on additive changes plus a deprecation window backed by CI schema checks
- 4. GraphQL loses when cache-by-URL, file transfer, or simple CRUD dominate the requirements
Interview Tips
- • Answer "when would you not use GraphQL" with a specific cost (CDN caching you would rebuild, resolver composition to staff, governance to run) instead of a vague pros and cons list
- • Walk a concrete screen: count the REST round trips, name the unused fields, then show the single query that replaces them
Cheat Sheet
REST vs GraphQL cheat sheet
Shapes
- REST: URL names the resource, server picks fields -> over-fetching
- GraphQL: POST /graphql, client picks fields in a selection set -> one round trip per screen
Caching
- GET + Cache-Control/ETag = free CDN caching
- POST bodies are not cacheable -> persisted queries, query-string GET, entity cache
Evolution
- REST: breaking change = new path (/v2), dual maintenance
- GraphQL: add nullable field -> @deprecated -> remove after migration; block breaks in CI
Keep REST when: CRUD + cache-by-URL, uploads/downloads, webhooks, single client.
Continue Learning
Before You Start
- No prerequisites listed.