Article
REST vs GraphQL: When to Use Each
A practical comparison of REST and GraphQL with real-world trade-offs.
The Setup: Why This Matters
Your frontend needs data. Your backend has it. How do they talk?
For decades, the answer was REST. You’d request /users/123 and get a user object. Simple, standard, understood by everyone.
Then GraphQL showed up and promised to solve REST’s biggest problems. Now teams face a choice: REST or GraphQL? Or both?
The honest answer: both have trade-offs. Neither is universally better. This article walks through the real-world scenarios where each shines.
REST: The Tried and True
REST (Representational State Transfer) treats your API as resources. Each resource has a URL.
GET /users/123 → Fetch user 123
POST /users → Create a new user
PUT /users/123 → Update user 123
DELETE /users/123 → Delete user 123
Simple. Stateless. HTTP methods map to operations. This is what most APIs look like today.
REST in Practice
A typical REST endpoint:
// Request
GET /api/users/123
// Response
{
"id": 123,
"name": "Sufi Afifi",
"email": "sufi@example.com",
"avatar": "https://...",
"role": "admin",
"department": "engineering",
"joinDate": "2022-03-15",
"reportsTo": 456
}
You get everything about the user, whether your frontend needs it or not. Downloading a user ID to show on a profile? You also got their email, avatar, department, and more.
Multiple Resource Problem
What if you need a user AND their posts AND comments on those posts?
In REST, you need multiple requests:
// Request 1: Get the user
GET /api/users/123
// Request 2: Get their posts
GET /api/users/123/posts
// Request 3: For each post, get comments
GET /api/posts/1/comments
GET /api/posts/2/comments
GET /api/posts/3/comments
For a screen showing a user profile with 10 posts and comments, you might make 12 requests. This is called the N+1 problem.
GraphQL: Asking for Exactly What You Need
GraphQL flips the model. Instead of fixed endpoints returning fixed data, the client asks for exactly what it needs.
query {
user(id: 123) {
id
name
posts {
id
title
comments {
id
text
}
}
}
}
You get back:
{
"data": {
"user": {
"id": 123,
"name": "Sufi Afifi",
"posts": [
{
"id": 1,
"title": "GraphQL vs REST",
"comments": [
{ "id": 10, "text": "Great post!" },
{ "id": 11, "text": "Thanks!" }
]
}
]
}
}
}
One request. One response. Exactly what you asked for.
GraphQL Query Examples
Basic query:
{
user(id: 123) {
name
email
}
}
With arguments:
{
posts(limit: 10, offset: 0) {
id
title
author {
name
}
}
}
With mutations (writes):
mutation {
createPost(title: "Hello", content: "World") {
id
createdAt
}
}
Fragments for reuse:
fragment UserInfo on User {
id
name
email
}
{
user1: user(id: 123) {
...UserInfo
}
user2: user(id: 456) {
...UserInfo
}
}
REST vs GraphQL: Side-by-Side Comparison
Scenario 1: Mobile App Showing a User Card
REST:
GET /api/users/123
// Gets: id, name, email, avatar, role, department, joinDate, reportsTo
// Shows: name, avatar
// Waste: 6 unused fields
GraphQL:
{
user(id: 123) {
name
avatar
}
}
Winner: GraphQL (smaller payload, faster on slow connections)
Scenario 2: Dashboard with 5 Different Data Sources
REST:
GET /api/users/me
GET /api/posts
GET /api/comments
GET /api/notifications
GET /api/analytics
// 5 requests
GraphQL:
{
me { ... }
posts { ... }
comments { ... }
notifications { ... }
analytics { ... }
}
Winner: GraphQL (one request, less latency)
Scenario 3: Public API with Caching Needs
REST:
GET /api/products/123
// Browser caches based on URL
GET /api/products/456
// Separate cache entries
GraphQL:
{
products(ids: [123, 456]) { ... }
}
Winner: REST (HTTP caching built-in; GraphQL requests always go to server)
The N+1 Problem: The Main Pain Point REST Solves
This is REST’s biggest weakness.
Imagine your API returns post summaries:
GET /api/posts
[
{ id: 1, title: "Post 1", authorId: 10 },
{ id: 2, title: "Post 2", authorId: 20 },
{ id: 3, title: "Post 3", authorId: 10 }
]
Your frontend needs author names. In REST, you must fetch each author:
// 1 request for posts
// + 2 more requests for unique authors (N+1 problem)
GET /api/authors/10
GET /api/authors/20
// If you had 100 posts from 50 authors: 1 + 50 = 51 requests!
GraphQL avoids this entirely:
{
posts {
id
title
author {
name
}
}
}
One request. The server resolves author data efficiently on its side.
Over-fetching and Under-fetching
Over-fetching (REST’s Problem)
You asked for a user; you got their entire profile including password hash (hopefully not!), internal IDs, deprecated fields, etc.
GET /api/users/123
{
id, name, email, avatar, role, department, joinDate,
reportsTo, salary, ssn, internalNotes, deprecated_field
// You only needed: id, name
}
GraphQL fixes this — ask for only what you need.
Under-fetching (REST’s Other Problem)
You asked for a post; you also need comments. So you make another request.
GET /api/posts/1 // Post, but no comments
GET /api/posts/1/comments // Now you have comments
// 2 requests for 1 logical entity
GraphQL fixes this — get related data in one query.
REST teams solve under-fetching with include parameters:
GET /api/posts/1?include=comments,author,tags
This works, but becomes unwieldy at scale and isn’t standardized.
Real-World Trade-offs
REST Advantages
-
HTTP Caching Works
GET /api/users/123 // Browser caches automatically // CDN can cache // Browsers respect Cache-Control headers -
Simpler to Understand
- Junior developers get it immediately
- No learning curve
- Debugging with curl is straightforward
-
Standard Tools
- Any HTTP tool works (curl, Postman, browser)
- Logging and monitoring are standard
- Firewalls and proxies understand it
-
Better for Public APIs
- Clients can implement their own caching
- Rate limiting per endpoint is simple
- Contract is explicit and stable
-
Easier Versioning
GET /api/v1/users/123 GET /api/v2/users/123
REST Disadvantages
- N+1 Problem — Need multiple requests
- Over-fetching — Get unnecessary data
- Under-fetching — Missing data, need more requests
- API Proliferation — Create custom endpoints (e.g.,
/api/user-with-posts-and-comments) - No Type Safety — Client doesn’t know response shape upfront
GraphQL Advantages
-
Exact Data
- Ask for what you need, get exactly that
- Smaller payloads, faster on mobile
-
Single Request
- No N+1 problem
- Less latency
- Better user experience
-
Type Safety
# The schema is the contract type User { id: ID! name: String! email: String } -
Introspection
- Clients can discover API shape at runtime
- IDE autocomplete works automatically
-
No Versioning
- Add new fields without breaking clients
- Deprecate old fields gracefully
GraphQL Disadvantages
-
Caching is Hard
- Every GraphQL request is a POST
- Browser caching doesn’t work by default
- Must implement custom caching logic
-
Complexity
- Server is complex (resolvers, dataloaders)
- Client needs GraphQL client library (Apollo, Urql)
- Debugging is harder
-
File Uploads
- REST:
multipart/form-datais standard - GraphQL: No standard yet (workarounds needed)
- REST:
-
Denial of Service Risk
- Query can request deeply nested data
users { friends { friends { friends { ... } } } }- Need query depth limits and timeouts
-
Overkill for Simple Endpoints
- If your API is just a few simple resources, GraphQL adds complexity with no benefit
When to Use REST
✅ Use REST when:
- You have a public API (caching matters)
- Your data access is simple (fixed endpoints, few relationships)
- Cache efficiency is critical (mobile, slow networks)
- Your team doesn’t know GraphQL
- You’re building a microservice (simple, focused)
- SEO matters (REST endpoints can be bookmarked, cached)
Examples: Twitter API (simplified), Stripe API, AWS APIs, most public APIs.
When to Use GraphQL
✅ Use GraphQL when:
- You have a diverse client base (mobile, web, dashboard, internal tools)
- N+1 problem is severe (complex data relationships)
- You want to avoid API versioning
- Your backend is a GraphQL-native database (Firebase Realtime, Hasura)
- Your team knows GraphQL and wants to use it
- You’re building an internal/private API
Examples: GitHub API, Shopify API, most modern startups.
Hybrid Approaches: Best of Both Worlds
Approach 1: REST + Caching
Use REST with aggressive caching:
GET /api/users/123
Cache-Control: max-age=3600
// Browser caches for 1 hour
// No server hit for repeat requests
Add query parameters for flexibility:
GET /api/users/123?fields=id,name,avatar
Approach 2: GraphQL + HTTP Caching
Cache GraphQL responses by query hash:
POST /graphql?query_hash=abc123
Cache-Control: max-age=300
// URL includes query hash
// Browser/CDN can cache
// Requires fixed queries (no variables)
Approach 3: REST + GraphQL Together
Expose both. Let clients choose.
REST: GET /api/users/123
GraphQL: POST /graphql
query { user(id: 123) { ... } }
Some endpoints REST, some GraphQL. Common in transitioning companies.
Approach 4: Backend For Frontend (BFF)
Create a dedicated endpoint for each client:
Mobile → GET /api/mobile/user (minimal fields, fast)
Web → GET /api/web/user (rich data)
Dashboard → GET /api/dashboard/user (aggregated data)
Or use one GraphQL endpoint that different clients query differently.
Migration Strategy: REST to GraphQL
If you want to move from REST to GraphQL, do it incrementally:
Phase 1: Parallel Endpoints
REST API running
GraphQL API running
Both backed by same database
Clients can call either
Phase 2: Migrate Clients
Desktop web → GraphQL
Mobile → GraphQL (smaller payloads)
Public API stays REST (caching, stability)
Phase 3: Deprecate REST
REST API deprecated after 6 months
All internal clients on GraphQL
Public API might stay REST forever
A real example:
// Old REST endpoint
GET /api/users/123/with-posts-and-comments
// New GraphQL endpoint
query {
user(id: 123) {
posts {
comments
}
}
}
// Old endpoint becomes GraphQL wrapper
GET /api/users/123/with-posts-and-comments
→ GraphQL query
→ same data structure
→ safe migration
Performance Comparison
Scenario: Fetch user + 10 posts + comments per post
REST (N+1 worst case):
Request 1: /api/users/123 → 2 KB
Request 2-11: /api/posts?user=123 → 20 KB total (2 KB per post)
Request 12-X: /api/comments?post=1..10 → 50 KB total
Total: 72 KB, 12 requests, 300ms latency
GraphQL (single request):
Request 1: /graphql (complex query) → 45 KB (only requested fields)
Total: 45 KB, 1 request, 150ms latency
But consider:
REST with Caching:
Request 1: /api/users/123 (CACHED) → 0 KB
Request 2: /api/posts (CACHED) → 0 KB
Request 3+: /api/comments (fresh) → 50 KB
Total: 50 KB, 1 network request, 50ms latency
Caching is powerful. Don’t underestimate it.
Common Mistakes
REST Mistakes
-
Inventing custom query syntax
// Bad GET /api/users?filters=role:admin,dept:engUse standards (JSON:API, OData, etc.) or GraphQL.
-
Ignoring caching
// Bad: Eternal cache, stale data Cache-Control: max-age=31536000 // Good: Validate with ETag Cache-Control: max-age=3600, must-revalidate ETag: "abc123" -
N+1 by default
// Bad: Returns 100 posts without authors GET /api/posts // Good: Offer ?include parameter GET /api/posts?include=author
GraphQL Mistakes
-
No Query Depth Limit
# Attacker sends deeply nested query { user { friends { friends { friends { ... } } } } } # Server exhaustedAlways add limits:
const depth = getQueryDepth(query); if (depth > 10) throw new Error("Query too deep"); -
No Dataloader
// Bad: N+1 on the server side posts.map(post => fetchAuthor(post.authorId)) // 100 DB queries // Good: Batch-load authors const authorLoader = new DataLoader(authorIds => db.authors.find({ id: { $in: authorIds } }) ); -
Ignoring Performance
// Bad: Full-text search in resolver resolve: (post) => { return db.comments.find({ text: { $regex: query } }) } // Good: Use indexed queries resolve: (post, args) => { return db.comments.find({ postId: post.id, ...args }) }
Decision Matrix
Choose based on your scenario:
| Factor | REST | GraphQL |
|---|---|---|
| Data relationships | Simple | Complex |
| Caching importance | High | Low |
| Team familiarity | High | Low |
| API stability | Frequent changes | Infrequent |
| Real-time features | Websockets | Subscriptions |
| File uploads | Native | Workaround |
| Public API | Good | Harder |
Summary
REST is a solid, proven choice. Use it when you want simplicity, caching, and standard HTTP semantics.
GraphQL shines when you have complex data relationships and want clients to request exactly what they need.
Reality: Most successful APIs use both. Twitter uses REST. GitHub uses both. Your startup can too.
The key is understanding the trade-offs:
- REST gains caching; GraphQL gains flexibility
- REST is simple; GraphQL is powerful
- REST is proven; GraphQL is modern
Choose based on your team’s skills, your data complexity, and what your clients need. And remember: you can always add GraphQL as a layer on top of REST if you change your mind.