GraphQL vs REST API Comparison: Tradeoffs for Modern Backends
Introduction
REST has been the default API style for decades—resources, HTTP verbs, predictable URLs. GraphQL (2015, Facebook) offers a single endpoint where clients request exactly the fields they need, with a typed schema and introspection.
Teams often debate migration: "Should we replace REST with GraphQL?" The answer depends on client diversity, caching needs, team expertise, and operational complexity. This article compares both with concrete examples, performance implications, and decision criteria—not GraphQL advocacy or REST nostalgia.
Table of Contents
- REST Fundamentals
- GraphQL Fundamentals
- Data Fetching and Over-Fetching
- Caching and CDN Behavior
- Versioning and Evolution
- Tooling and Developer Experience
- Performance and Security Considerations
- Best Practices
- Common Mistakes
- Interview Questions
- Summary
- References
REST Fundamentals
Resources identified by URLs; HTTP methods express intent:
GET /api/users/1 → fetch user
GET /api/users/1/orders → fetch user's orders
POST /api/orders → create order
PATCH /api/users/1 → partial update
DELETE /api/users/1 → delete user
Responses are server-defined—client gets full resource representation:
{
"id": "1",
"name": "Monu Kumar",
"email": "monu@example.com",
"created_at": "2024-01-15T10:00:00Z",
"internal_notes": "VIP"
}
Clients may over-fetch fields they don't display or under-fetch and need multiple round trips.
GraphQL Fundamentals
Single endpoint (typically POST /graphql); client sends query:
query UserDashboard($userId: ID!) {
user(id: $userId) {
name
orders(limit: 5) {
id
total
items {
productName
}
}
}
}
Server resolves fields via resolvers:
const resolvers = {
Query: {
user: (_, { id }) => db.users.findById(id),
},
User: {
orders: (user, { limit }) => db.orders.findByUser(user.id, limit),
},
Order: {
items: (order) => db.orderItems.findByOrder(order.id),
},
};
Schema is strongly typed and introspectable—powers GraphiQL, codegen, and validation.
Data Fetching and Over-Fetching
REST pain: mobile app needs user name + 3 order totals:
GET /users/1
GET /users/1/orders?limit=3
Two round trips; first response includes unused fields.
GraphQL: one request, shaped response.
REST solutions without GraphQL:
- Sparse fieldsets —
GET /users/1?fields=name,email - BFF (Backend for Frontend) — tailored endpoint per client
- JSON:API compound documents with
include
GraphQL solves flexibility elegantly but shifts complexity to server resolver layer and N+1 query risk.
Caching and CDN Behavior
REST leverages HTTP caching naturally:
GET /api/products/99
Cache-Control: public, max-age=3600
ETag: "abc123"
CDN and browser cache by URL + method. GraphQL typically POSTs to one URL—HTTP cache ineffective for queries.
GraphQL caching options:
- Normalized client cache (Apollo, urql)—cache by entity ID
- Persisted queries — GET with query hash
- Server-side Redis per resolved query hash
- @cacheControl directives in schema (Apollo Server)
REST wins for public, cacheable read-heavy content at the edge.
Versioning and Evolution
REST versioning strategies:
- URL:
/v1/users,/v2/users - Header:
Accept: application/vnd.api+json; version=2 - New resources without breaking old clients
GraphQL favors continuous evolution:
- Add fields—old clients ignore
- Deprecate fields with
@deprecated—remove after sunset period - Avoid breaking changes to existing field types
Both require discipline. GraphQL deprecation is visible in schema; REST breaking changes need documentation and client coordination.
Tooling and Developer Experience
| Capability | REST | GraphQL |
|---|---|---|
| Discovery | OpenAPI/Swagger | Introspection + schema |
| Client codegen | openapi-generator | graphql-codegen |
| Validation | JSON Schema / OpenAPI | GraphQL schema |
| Real-time | WebSockets separate | Subscriptions in schema |
| File upload | Standard multipart | Multipart spec / separate REST |
GraphQL subscriptions unify real-time in schema; REST often adds separate WebSocket/SSE endpoints.
Learning curve: GraphQL requires understanding resolvers, N+1, dataloaders—steeper for backend newcomers.
Performance and Security Considerations
N+1 problem:
query {
users { orders { items { product { name } } } }
}
Naive resolvers query DB per user/order—use DataLoader:
const orderLoader = new DataLoader(async (userIds) => {
const orders = await db.orders.findByUserIds(userIds);
return userIds.map((id) => orders.filter((o) => o.userId === id));
});
Security:
- Query depth/complexity limits — prevent expensive queries
- Rate limiting by cost, not just request count
- Authorization per field — not just endpoint level
- Introspection disabled in production sometimes
REST attack surface is more predictable; GraphQL needs explicit query cost analysis.
Best Practices
- Choose REST for simple CRUD, public CDN caching, file uploads, wide third-party integration.
- Choose GraphQL for diverse clients (web + mobile + partners) with different data needs.
- Hybrid is valid—GraphQL for app API, REST for public/partner APIs.
- Don't migrate for hype—cost of rewrite and ops burden are real.
- Invest in tooling whichever you pick—OpenAPI or schema CI checks.
- Monitor p95 latency and error rates per operation (GraphQL) or route (REST).
Common Mistakes
- GraphQL for everything including simple internal CRUD with one client.
- Ignoring N+1 — DB melts after GraphQL launch.
- No query limits — one client takes down API.
- REST with 50 nested subresources — should have been GraphQL or BFF.
- Caching assumptions — GraphQL without client normalized cache still hits server every time.
- Exposing internal schema without field-level auth.
Interview Questions
- When would you choose GraphQL over REST?
- Explain over-fetching and under-fetching.
- How does HTTP caching differ between REST and GraphQL?
- What is the N+1 problem in GraphQL?
- GraphQL versioning vs REST versioning?
- How to secure a public GraphQL API?
- What is a BFF and when is it enough?
- GraphQL subscriptions vs REST polling?
Federation and Schema Stitching
Large organizations split GraphQL across teams:
[Users subgraph] + [Orders subgraph] → [Gateway] → Client
Apollo Federation or GraphQL Mesh compose subgraphs into unified schema. Each team owns resolvers and deployment; gateway routes fields to correct service.
Tradeoff: operational complexity—gateway versioning, schema registry, and coordinated deprecations become platform team responsibilities.
GraphQL at the Edge
Some CDNs and edge platforms support cached GraphQL at edge with persisted queries—rare compared to REST CDN caching but emerging for public read-heavy APIs.
Evaluate whether GraphQL benefits justify edge investment versus static REST resources with long Cache-Control.
Migration Strategies
Strangler pattern: expose GraphQL alongside REST; migrate clients incrementally:
- Wrap existing REST services with GraphQL resolvers (no DB rewrite)
- Move high-churn clients (mobile) first
- Deprecate REST endpoints after traffic drops below threshold
- Consolidate resolvers to direct DB access where REST wrapper added latency
Avoid big-bang rewrites—dual maintenance period is normal for 12–24 months at scale.
GraphQL Error Handling vs REST
REST uses HTTP status per resource:
HTTP/1.1 404 Not Found
{"error": "User not found"}
GraphQL often returns 200 with errors array:
{
"data": { "user": null },
"errors": [{ "message": "User not found", "path": ["user"], "extensions": { "code": "NOT_FOUND" } }]
}
Clients must handle partial success—some fields resolve while others fail. Design UX for degraded GraphQL responses.
Choosing for Your Team
Decision matrix:
- One web client, CRUD API → REST + OpenAPI
- Mobile + web + partner dashboards, different fields → GraphQL or BFFs
- Public CDN-heavy blog → REST
- Internal admin with complex relations → GraphQL
Revisit decision annually—product and team composition change; wrong choice from year two may be right in year five.
Summary
REST excels at simplicity, HTTP caching, and universal tooling. GraphQL excels at flexible client-driven queries and typed schemas with introspection. Many successful products use both—or REST with BFFs instead of full GraphQL. Match the API style to client needs, caching requirements, and team capacity—not industry trends alone.