REST vs GraphQL
Learn when REST's HTTP caching wins over GraphQL, when GraphQL's typed queries earn their complexity cost, and how to defend either API choice in a system design interview.
TL;DR
- Use REST when building a public API for third-party developers, when HTTP caching is critical to your scaling strategy, or when your data model is simple and flat.
- Use GraphQL when multiple client types (web, iOS, Android) need different data shapes from the same underlying data, or when your product surface is deeply relational and request waterfalls are hurting mobile performance.
- REST gives you HTTP verbs, URL-based caching, and universal toolset compatibility for free. GraphQL gives you precise field selection, a self-documenting type system, and one endpoint to serve all query shapes.
- The most common mistake is picking GraphQL because it feels modern. The most common mistake with REST is proliferating versioned endpoints (/v1, /v2, /v3) until the contract surface becomes unmanageable.
- Most mature systems end up using both: REST for webhooks and auth flows, GraphQL for complex data-fetching UI surfaces.
The Framing
In 2012, Facebook was shipping mobile apps over 2G and 3G networks in developing markets. Each News Feed required fetching posts, reactions, comments, author profiles, and friend data. With REST, that was 5 to 8 separate round trips. On a 400ms-latency 3G connection, the waterfall added 2 to 3 full seconds to every feed load.
Lee Byron's team at Facebook didn't set out to create a new query language. They set out to solve a specific problem: how do you give a mobile client exactly the data it needs, in one round trip, across deeply nested relational data? GraphQL was their answer, shipping internally in 2012 and publicly in 2015.
The key insight is that REST and GraphQL aren't solving the same problem. REST is an architectural style centered on resources; GraphQL is a query language for fetching precisely shaped data. That distinction is what separates a forgettable interview answer from an impressive one.
How Each Works
REST: Resources, verbs, and URLs
REST (Representational State Transfer) structures an API around resources addressed by URLs. You interact with them using HTTP verbs: GET to read, POST to create, PUT/PATCH to update, DELETE to remove.
The beauty of REST is that it builds directly on HTTP. A GET /products/42 response can carry Cache-Control: max-age=300, and every CDN, browser, and reverse proxy in the world automatically caches it. The caching infrastructure already knows what to do because the HTTP semantics are explicit.
REST's limitation is the mismatch between resource boundaries and client needs. Your API was designed around what data exists, not around what any particular screen needs. A mobile profile page that needs user data, their last 5 posts, and 3 mutual friends must make 3 separate requests. Or you build a custom endpoint that denormalizes everything for that screen, which is just a different set of maintenance problems.
# REST: three requests to build one profile screen
GET /users/123 # Fetch user data
GET /users/123/posts # Can fire in parallel with /friends
GET /users/123/friends # Can fire in parallel with /posts
# Total: 3 network requests even when parallelized — 3x the latency budget
# vs. 1 GraphQL request returning all three in one round trip
GraphQL: A typed query language
GraphQL is a query language running over a single HTTP endpoint (/graphql). Instead of the server deciding what data shape to return, the client specifies exactly which fields it needs in a typed query. The server parses that query, executes resolvers for each field, and returns exactly what was asked for.
The type system is the core differentiator. Every field in a GraphQL API is defined in a schema with explicit types. This schema is the contract between client and server. Clients can introspect the schema at runtime, new fields can be added without breaking existing queries, and old fields can be deprecated without version bumps.
GraphQL collapses the multi-round-trip problem into a single POST request. A profile page query fetches user, posts, and friends in one call. The server runs sub-resolvers, potentially in parallel, and returns a response shaped exactly like the query.
# GraphQL: one request for the same profile screen data
query ProfilePage($userId: ID!) {
user(id: $userId) {
name
avatar
posts(first: 5) {
title
createdAt
}
friends(first: 3) {
name
avatar
}
}
}
# Result: one network request. Client gets exactly these fields, nothing more.
Head-to-Head Comparison
| Dimension | REST | GraphQL |
|---|---|---|
| API paradigm | Resource-oriented. Each URL is one noun; HTTP verbs define the action. | Query language. One endpoint. Client defines the response shape per request. |
| Data fetching | Fixed data shape per endpoint. Over-fetching and under-fetching are structural. | Exact field selection per query. Client gets what it requests and nothing more. |
| HTTP caching | Native. GET requests cache at browser, CDN, and reverse proxy for free. | No built-in HTTP caching. POST-based by default. Requires persisted queries or app-level caching to compensate. |
| Type system | Optional. OpenAPI/Swagger adds schema, but not enforced at runtime. | Mandatory. Schema is the API contract, introspectable, and validated at execution time before any resolver runs. |
| Versioning | URL-based (/v1, /v2) or header-based. Creates friction and maintenance overhead at scale. | No versioning needed. Add fields (non-breaking), deprecate old ones. Schema evolves without version bumps. |
| Error handling | Standard HTTP status codes (200, 404, 500). Infrastructure can route and alert on status. | Returns HTTP 200 for application-level resolver errors, even when data is null. Malformed queries typically return 4xx. Monitoring must parse the response body — HTTP status codes are unreliable failure signals. |
| N+1 query risk | Minimal. Each endpoint is purpose-built for a known access pattern. | High without DataLoader. Naive resolvers fire one DB query per parent entity, multiplying database load. |
| Authorization | Per-endpoint. One middleware call per route. Simple and predictable. | Per-field. Each field resolver may need separate auth checks. Complexity scales with schema depth. |
| File uploads | Trivially multipart/form-data on a POST endpoint. Browsers and CDNs handle it natively. | Non-standard. Requires a multipart spec that varies by library. Most teams keep file uploads as REST endpoints. |
| Real-time (subscriptions) | SSE or polling over HTTP. WebSockets via separate connection layer. | Native subscription type. Still requires WebSocket underneath but is expressed in the same schema. |
| Developer onboarding | Familiar. HTTP and JSON is the universal baseline. Every team and language knows it. | Requires learning GraphQL syntax, concepts (resolvers, schema, fragments). Steeper initial ramp. |
| Best fit | Public APIs, simple CRUD services, CDN-heavy read workloads, external integrations, file I/O. | Complex UI data-fetching, multi-client APIs, deeply relational data, mobile-first products. |
The N+1 Problem
This is the gotcha that bites every team adopting GraphQL for the first time. If you don't address DataLoader from day one, your GraphQL API will perform worse than the REST API it replaced.
Here's the problem. Suppose a query asks for { posts { author { name } } }. The server resolves posts, getting back 10 post objects.
For each post, the author resolver naively does SELECT * FROM users WHERE id = $1. With 10 posts, that's 10 separate queries: 1 for posts plus N for authors, hence "N+1."
sequenceDiagram
participant C as Client
participant G as GraphQL Server
participant D as Database
Note over C,D: posts(first: 10)<br/>{ author { name } }
C->>G: POST /graphql
G->>D: SELECT * FROM posts LIMIT 10
D-->>G: 10 post rows
alt Without DataLoader (N+1)
loop for each of 10 posts
G->>D: SELECT * FROM users WHERE id=?
D-->>G: 1 row
end
Note over G,D: 11 total DB queries
else With DataLoader
G->>D: SELECT * FROM users WHERE id IN (1..10)
D-->>G: 10 rows, one round trip
Note over G,D: 2 total DB queries
end
G-->>C: HTTP 200, same response shape either way
DataLoader is the fix: a batching utility that collects individual resolver calls within a single event loop tick, then fires one IN query. With DataLoader, the same query runs 2 DB queries (1 for posts, 1 for all unique authors) instead of 11. At production traffic, that difference is what determines whether your database handles the load.
The rule to state in every GraphQL answer: every resolver that fetches by a single ID must use DataLoader. No exceptions.
When REST Wins
So when does REST win decisively? More often than the industry discourse suggests.
Use REST when:
- You're building a public API for third-party developers. Stripe, Twilio, and Shopify Payments all run REST APIs that developers love. GraphQL adds a schema learning curve that narrows adoption among developers who just want
curl. - HTTP caching matters to your scaling strategy. A product page response at
GET /products/seasonal-salewithCache-Control: max-age=300gets served free by every Cloudflare edge node. No origin load on Black Friday. That's not available to a POST-based GraphQL endpoint by default. - Your data is simple and flat with predictable shapes. Payment processing, order management, authentication flows, and user CRUD are all resource-oriented operations. REST is the right shape for them.
- You need file uploads, webhooks, or binary streaming. REST handles these naturally via
multipart/form-dataand HTTP streaming. GraphQL requires per-library workarounds for all three. - Your team is small or early-stage. REST has less cognitive overhead. I've seen startups add GraphQL at 5 engineers and spend the first month debugging resolver waterfalls instead of shipping product. Start simple.
- Compliance and audit requirements are a priority. Authentication middleware, rate limiting, and audit logging are simpler when each operation maps to one URL with one middleware chain.
For your interview: when defending REST, the specific sentence is "80% of complex data-fetching problems can be solved with REST plus a BFF layer that aggregates server-side." That shows you understand both options and aren't defaulting to REST out of habit.
When GraphQL Wins
GraphQL earns its complexity cost in specific, well-defined scenarios. The mistake is applying it everywhere.
Use GraphQL when:
- Multiple client types need different data shapes from the same API. Web apps, iOS, and Android often show different subsets of the same data. GraphQL lets each client define the exact fields it needs without creating separate endpoints per client.
- Mobile clients are on constrained bandwidth. At 10M daily mobile users, shaving 30% off average response size reduces real user costs and improves perceived latency on cellular. The extra bytes from over-fetching are not free at scale.
- Your data model is deeply relational and screens require traversing several levels. Social feeds, repository explorers, and e-commerce product pages (product with variants, inventory, reviews, related items) all benefit from single-query traversal.
- Frontend teams need to iterate faster than backend teams can. With REST, adding a new field to a screen usually requires a backend change and deploy. With GraphQL, the frontend modifies its query and gets the new shape immediately, assuming the field exists in the schema.
- You're running a developer platform where consumers are your own teams. GitHub's internal teams use GraphQL for new integrations because schema introspection and auto-generated typed clients reduce coordination overhead.
The calculus to use in interviews: "I'd choose GraphQL when I have at least two client platforms with different data needs, and my screens require data from three or more related entities per render. Below that threshold, REST is simpler and equally effective."
The Nuance
The real-world answer is almost never "pure REST" or "pure GraphQL." Here's how mature teams actually land.
Most teams run both. GraphQL for complex UI data-fetching. REST for webhooks, authentication flows (/oauth/token, /logout), file uploads, and machine-to-machine integrations. A common production setup: GraphQL endpoint for web and mobile frontends, REST for the public developer API and all B2B integrations.
The BFF pattern often makes the question moot. A Backend For Frontend (BFF) is a thin server-side aggregation layer per client type: one for web, one for mobile. The BFF fetches from multiple internal REST services and returns a client-optimized shape. You get REST's simplicity and GraphQL's tailored responses, without any GraphQL on the wire. Netflix, SoundCloud, and most large organizations use BFF over public-facing GraphQL.
Persisted queries change the caching calculus. If HTTP caching is why you're avoiding GraphQL, persisted queries are the production answer: pre-register your query strings on the server and have clients send only a query hash as a GET parameter. The response becomes cacheable by query ID and variables at CDN edge. Shopify Storefront API and Facebook both do this in production. More operational overhead than vanilla GraphQL, but it gives you GraphQL's type safety combined with REST's caching behavior.
Decision Guide
These two diagrams serve as reference tools for choosing at design time.
Real-World Examples
GitHub: REST v3 vs GraphQL v4 (same company, two coexisting APIs)
GitHub maintained a public REST API for years. In 2016 they shipped a GraphQL API (v4) alongside it. The stated reason: REST v3 was returning entire repository objects when integrations only needed a field or two. GitHub's own website was firing 20 or more REST API calls per page render.
The v4 GraphQL API let developers request exactly what they needed. GitHub's own web app moved internal fetching to v4 and reduced data transfer per page significantly. Today REST v3 remains the primary API for third-party integrations. GraphQL v4 is the primary API for GitHub's own frontends. Same data, different API surface for different consumers.
Shopify: persisted queries at 50,000 requests per second
Shopify's Storefront API is a public GraphQL API used by hundreds of thousands of storefronts. Every product page is GraphQL-powered, but the underlying caching uses REST-like semantics: pre-registered query hashes sent as GET parameters, responses cached at CDN edge by hash plus variables. Peak product page traffic hits the CDN, not Shopify's origin. GraphQL type safety and field selection at the application layer, REST caching semantics at the infrastructure layer.
Twitter (X): REST public, GraphQL internal
Twitter's public v2 API remains REST. The developer ecosystem built on GET /2/tweets/:id isn't interested in learning a new query language. Internally, Twitter's web and iOS clients use GraphQL with federation to power the timeline and compose views. Neither API style won company-wide: each serves the consumers it was designed for.
The pattern across all three: GraphQL for complex internal data-fetching by frontend teams you control, REST for public-facing developer APIs and machine-to-machine integrations. I keep coming back to these cases whenever a team asks me to adjudicate the question from scratch.
How This Shows Up in Interviews
Here's the honest pattern: most interviewers asking "REST or GraphQL?" are testing whether you understand the trade-offs or just have a preference. Picking one without explaining why is the wrong answer.
The phrase that earns credit
Say: "It depends on the consumers. If this is a single client type with straightforward CRUD needs, REST is the right default. If we have multiple clients needing different data shapes from relational data, GraphQL earns its operational complexity. Let me think about which case we're in." That reasoning process, spoken out loud, is what the interviewer evaluates.
The N+1 question follows every GraphQL proposal
If you propose GraphQL, the very next probe is almost always the N+1 problem. Know the answer cold: without DataLoader, naive resolvers fire one query per parent object. DataLoader batches all same-tick resolver calls into one IN query. Required for every resolver that touches a database. Say "I'd add DataLoader for all nested resolvers" in the same breath as "I'd use GraphQL here."
Depth expected at senior/staff level:
- Explain why GraphQL defaults to POST and what that costs in HTTP caching infrastructure.
- Know the persisted queries pattern and when it applies (public GraphQL APIs needing CDN caching).
- Explain the BFF alternative and when it's simpler than full GraphQL adoption.
- Know that GraphQL returns HTTP 200 for application-level errors and explain what that means for monitoring strategy.
- Describe query complexity scoring for rate limiting on public GraphQL APIs.
Common follow-up questions and strong answers:
| Interviewer asks | Strong answer |
|---|---|
| "How would you cache a GraphQL API?" | "By default, POST requests don't cache at HTTP layer. Two fixes: use GET for read-only queries with variables URL-encoded (URL length limits apply), or use persisted queries where clients send a pre-registered hash as GET parameter. App-level caching via Redis on DataLoader is also common for expensive computed fields." |
| "What's the N+1 problem in GraphQL?" | "If posts return 10 items and each post's author is resolved separately, that's 11 DB queries. DataLoader batches all resolver calls within one event loop tick into a single IN query. Two queries total. Every resolver fetching by ID must use DataLoader." |
| "When would you NOT use GraphQL?" | "Public APIs for third-party developers, CDN-heavy read workloads, file upload endpoints, webhook delivery, simple CRUD services with flat data models, and early-stage teams where the complexity cost isn't justified yet." |
| "How do you handle auth in GraphQL?" | "Two levels: operation-level middleware (can this user run this query) and field-level resolvers (can this user see this field). Operation-level is header validation middleware. Field-level requires per-resolver checks, which grows with schema size. Libraries like graphql-shield formalize the rules. REST's per-endpoint middleware is simpler to reason about at lower complexity." |
| "REST or GraphQL for design-twitter?" | "Feed fetching internally: GraphQL, because a tweet requires tweet data, author profile, media, and engagement counts in one render. Public API for third-party clients: REST, because developers expect HTTP and GraphQL adds onboarding friction. Both coexist in the real Twitter architecture." |
Test Your Understanding
Quick Recap
- REST structures its API around resources and HTTP verbs. GraphQL is a typed query language where clients specify exactly which fields they need, collapsing multiple round trips into one.
- REST's biggest structural advantage is HTTP caching: GET requests cache at browser, CDN, and reverse proxy level with zero additional work. GraphQL's POST-by-default design opts out of that infrastructure layer.
- GraphQL's biggest structural advantage is eliminating over-fetching and under-fetching for multi-client APIs. One query replaces 3 to 5 round trips with a single typed request.
- The N+1 problem is GraphQL's most common performance failure. Every list resolver fetching related entities must use DataLoader, or naive resolvers fire one DB query per parent object, multiplying database load by N.
- GraphQL returns HTTP 200 for application-level resolver errors, even when
datais null or partial. Your monitoring must parse theerrorsarray in the response body — HTTP status code-based alerting does not carry over from REST. - Production-grade GraphQL APIs address the caching gap with persisted queries: pre-registered query hashes sent as GET parameters, cacheable at CDN edge. Shopify Storefront API is the canonical reference implementation.
- Most mature systems run both: GraphQL for complex internal UI data-fetching and REST for webhooks, auth, file uploads, public developer APIs, and service-to-service communication.
Related Trade-offs
- Microservices vs Monolith — GraphQL federation is a microservices pattern. Understanding service boundaries clarifies where schema ownership lives across teams and services.
- Push vs Pull — GraphQL subscriptions are a push pattern. Before wiring up subscriptions, understanding the broader push vs pull decision helps you avoid adding WebSocket infrastructure when SSE or polling is simpler.
- SQL vs NoSQL — The N+1 problem is fundamentally a database access pattern issue. Understanding your database's batch query capabilities shapes how DataLoader and batching strategies are implemented.
- API Gateway — GraphQL gateways (Apollo Federation, Hasura) sit in the same architectural position as API gateways. Understanding gateway responsibilities clarifies where to implement authentication, rate limiting, and query complexity scoring.
- Caching — GraphQL's caching story is more complex than REST's. Application-level caching with Redis on top of DataLoader is how production systems compensate for the absence of HTTP-layer caching.