Graphql vs REST Deep Dive — When to Use Each with Real Examples
In this tutorial, you'll learn about GraphQL vs REST Deep Dive. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
GraphQL and REST are API architecture styles that differ fundamentally in how clients request data, with REST exposing multiple fixed endpoints and GraphQL exposing a single endpoint where clients specify exactly which fields they need.
What You'll Learn
By the end of this tutorial, you will understand the architectural differences between GraphQL and REST, when each is the better choice, how caching and tooling differ, and how to migrate an existing REST API to GraphQL incrementally.
Why It Matters
Choosing the wrong API architecture leads to over-fetching or under-fetching data, poor developer experience, and difficult scaling. Doda Browser uses a combination where public data is served via REST for CDN caching benefits while internal tools use GraphQL for flexible dashboard queries.
Real-World Use
A mobile app displays user profiles with avatar, name, and recent posts. With REST, this requires three API calls (/users/:id, /users/:id/avatar, /users/:id/posts). With GraphQL, one query fetches exactly the needed fields, reducing mobile network latency.
Data Fetching Comparison
flowchart LR
subgraph "REST"
C1[Client] --> E1[/users]
C1 --> E2[/users/id/posts]
C1 --> E3[/users/id/followers]
end
subgraph "GraphQL"
C2[Client] --> E4[/GraphQL]
E4 --> Q[Query: {user {name posts followers}}]
end
style C2 fill:#22c55e,color:#fff
REST API Example (Node.js)
// rest-api.js
// Express REST API with multiple endpoints
const express = require('express');
const app = express();
const users = [
{ id: 1, name: 'Alice', email: 'alice"@example".com', role: 'admin', posts: 42, followers: 128 },
{ id: 2, name: 'Bob', email: 'bob"@example".com', role: 'user', posts: 17, followers: 56 },
];
// GET /api/users — list all users
app.get('/api/users', (req, res) => {
console.log(`[REST] GET /api/users — returns ${users.length} users`);
res.json(users);
});
// GET /api/users/:id — single user (over-fetches email for list views)
app.get('/api/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) return res.status(404).json({ error: 'User not found' });
console.log(`[REST] GET /api/users/${req.params.id}`);
res.json(user); // Always returns all fields, even if client only needs name
});
// GET /api/users/:id/posts — user's post count
app.get('/api/users/:id/posts', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) return res.status(404).json({ error: 'User not found' });
console.log(`[REST] GET /api/users/${req.params.id}/posts`);
res.json({ userId: user.id, postCount: user.posts });
});
app.listen(3000, () => console.log('REST API on http://localhost:3000'));
Expected behavior: The REST API exposes three endpoints. Each endpoint returns a fixed response structure. A client that only needs the user's name receives the entire user object (over-fetching). A client needing user + posts makes two HTTP calls (under-fetching).
GraphQL API Example (Node.js)
// graphql-api.js
// Apollo GraphQL server with flexible queries
const { ApolloServer, gql } = require('apollo-server');
const users = [
{ id: 1, name: 'Alice', email: 'alice"@example".com', role: 'admin', posts: 42, followers: 128 },
{ id: 2, name: 'Bob', email: 'bob"@example".com', role: 'user', posts: 17, followers: 56 },
];
const typeDefs = gql`
type User {
id: ID!
name: String!
email: String!
role: String!
posts: Int!
followers: Int!
}
type Query {
users: [User!]!
user(id: ID!): User
}
`;
const resolvers = {
Query: {
users: () => {
console.log(`[GraphQL] Resolving users — returns ${users.length}`);
return users;
},
user: (_, { id }) => {
console.log(`[GraphQL] Resolving user ${id}`);
return users.find(u => u.id === parseInt(id));
},
},
};
const server = new ApolloServer({ typeDefs, resolvers });
server.listen(4000).then(() => console.log('<a href="/apis/graphql/">GraphQL</a> on http://localhost:4000'));
Expected behavior: The GraphQL server exposes a single /<a href="/apis/Graphql/">Graphql</a> endpoint. The client controls the response shape:
# Client sends this query — only requests name field
query {
user(id: 1) {
name
}
}
# Response includes only the requested field:
# { "data": { "user": { "name": "Alice" } } }
No over-fetching, no under-fetching. The client declares its data requirements, and the server responds with exactly those fields.
Caching Strategies Compared
REST benefits from HTTP caching at every layer: CDN caches GET responses by URL, browsers cache resources, and reverse proxies cache with simple URL-based keys. GraphQL POST queries all hit the same URL, making HTTP caching impossible without additional tooling.
# rest_caching.py
# REST API caching strategies
from flask import Flask, jsonify, request
from functools import lru_cache
import time
app = Flask(__name__)
# Simulated database
articles_db = {
1: {"id": 1, "title": "REST vs GraphQL", "views": 1200, "updated_at": "2026-06-20"},
2: {"id": 2, "title": "API Caching Best Practices", "views": 890, "updated_at": "2026-06-21"},
}
@lru_cache(maxsize=128)
def get_article_from_db(article_id):
"""Simulate slow database query."""
time.sleep(0.5)
return articles_db.get(article_id)
@app.route('/api/articles/<int:article_id>')
def get_article(article_id):
"""
REST endpoint benefits from HTTP caching.
CDN caches by URL: /api/articles/1 is always the same resource.
"""
article = get_article_from_db(article_id)
if not article:
return jsonify({"error": "Not found"}), 404
response = jsonify(article)
response.headers['Cache-Control'] = 'public, max-age=300'
response.headers['ETag'] = f'"{article["updated_at"]}"'
print(f"[REST] Served article {article_id} with caching headers")
return response
@app.route('/graphql', methods=['POST'])
def graphql_endpoint():
"""
GraphQL endpoint: all queries hit the same URL.
Cannot cache by URL alone. Must use persisted queries or
automatic persisted queries (APQ) for CDN caching.
"""
query = request.json.get('query', '')
print(f"[GraphQL] All queries hit /graphql — URL-based caching impossible")
print(f" Query: {query[:50]}...")
return jsonify({"data": {"message": "GraphQL response"}})
if __name__ == '__main__':
app.run(port=5000)
Expected output:
[REST] Served article 1 with caching headers
[GraphQL] All queries hit /graphql — URL-based caching impossible
Query: query { articles { title } }
REST leverages URL-based caching naturally. GraphQL requires automatic persisted queries (APQ) or a CDN that understands GraphQL POST bodies for caching.
When to Use REST
REST excels when caching is critical, you need broad HTTP tooling compatibility, and your resources have well-defined representations.
# rest_vs_graphql_decisions.py
# Decision framework for REST vs GraphQL
def choose_api_style(requirements):
"""Score-based decision helper for REST vs GraphQL."""
rest_score = 0
graphql_score = 0
# Caching requirements
if requirements.get('cdn_caching'):
rest_score += 3
print("+ REST: Native CDN caching with URL-based cache keys")
# Multiple data sources
if requirements.get('aggregate_sources'):
graphql_score += 3
print("+ GraphQL: Single query resolves data from multiple sources")
# Mobile clients
if requirements.get('mobile_clients'):
graphql_score += 2
print("+ GraphQL: Avoids under-fetching on slow mobile networks")
# Public API
if requirements.get('public_api'):
rest_score += 2
print("+ REST: Universal compatibility, simpler documentation")
# Rapid frontend iteration
if requirements.get('rapid_frontend'):
graphql_score += 2
print("+ GraphQL: Frontend adds fields without backend changes")
print(f"\nREST score: {rest_score}")
print(f"GraphQL score: {graphql_score}")
return "REST" if rest_score >= graphql_score else "GraphQL"
scenario = {
"cdn_caching": True,
"aggregate_sources": False,
"mobile_clients": True,
"public_api": True,
"rapid_frontend": False,
}
print(f"Recommendation: {choose_api_style(scenario)}")
Expected output:
+ REST: Native CDN caching with URL-based cache keys
+ GraphQL: Avoids under-fetching on slow mobile networks
+ REST: Universal compatibility, simpler documentation
REST score: 5
<a href="/apis/graphql/">GraphQL</a> score: 2
Recommendation: REST
When to Use GraphQL
GraphQL is ideal for complex data graphs, mobile applications where network efficiency matters, and scenarios where frontend teams iterate quickly without backend coordination.
// Graphql-nested-query.js
// Graphql resolves deeply nested data in one round trip
const { ApolloServer, gql } = require('apollo-server');
const authors = [
{ id: 1, name: 'Alice', books: [1, 2] },
{ id: 2, name: 'Bob', books: [3] },
];
const books = [
{ id: 1, title: 'Graphql in Action', authorId: 1, reviews: [1] },
{ id: 2, title: 'REST Api Design', authorId: 1, reviews: [2] },
{ id: 3, title: 'Node.js Patterns', authorId: 2, reviews: [] },
];
const reviews = [
{ id: 1, bookId: 1, text: 'Excellent guide', rating: 5 },
{ id: 2, bookId: 2, text: 'Good reference', rating: 4 },
];
const typeDefs = gql`
type Author { id: ID! name: String! books: [Book!]! }
type Book { id: ID! title: String! author: Author! reviews: [Review!]! }
type Review { id: ID! text: String! rating: Int! }
type Query { authors: [Author!]! }
`;
const resolvers = {
Author: {
books: (author) => books.filter(b => author.books.includes(b.id)),
},
Book: {
author: (book) => authors.find(a => a.id === book.authorId),
reviews: (book) => reviews.filter(R => book.reviews.includes(R.id)),
},
Query: {
authors: () => authors,
},
};
new ApolloServer({ typeDefs, resolvers }).listen(4000);
Expected behavior: A single GraphQL query fetches authors, their books, and book reviews in one round trip. With REST, this would require 1 + N + M endpoints (authors, then books per author, then reviews per book), leading to the N+1 Problem.
Common Errors
1. Treating GraphQL as a Database Replacement
GraphQL resolvers should not mirror database tables directly. Each resolver is a function that can call any data source. Exposing database structure through GraphQL creates security and coupling issues — always add a business logic layer between the database and resolvers.
2. Ignoring the N+1 Problem in GraphQL
GraphQL resolvers run once per parent object. Without batching (DataLoader), querying a list of 100 items triggers 101 database queries (1 for the list, 100 for nested fields). Always use DataLoader to batch and cache resolver calls.
3. Exposing Mutations Without Rate Limiting
GraphQL mutations can be more expensive than REST POST requests because a single mutation can create, update, and delete multiple resources. Apply per-query complexity limits and Rate Limiting at the GraphQL layer, not just at the HTTP level.
4. Not Using Persisted Queries in Production
Without persisted queries, every GraphQL request sends the full query string, increasing bandwidth and preventing CDN caching. Use persisted queries or automatic persisted queries (APQ) to send only the query hash, with the full query stored on the server.
5. Over-fetching in REST Due to Fixed Responses
REST endpoints return fixed response objects. When a frontend only needs a user's avatar URL but the endpoint returns the full user profile, mobile clients waste bandwidth. Consider using REST with sparse fieldsets (?fields=name,avatar) or migrate to GraphQL for data-heavy applications.
6. Mixing GraphQL and REST Without Clear Boundaries
When both styles coexist, teams often duplicate business logic or create confusing APIs where some operations use REST and others use GraphQL inconsistently. Define clear rules: REST for public APIs and CDN-cached content, GraphQL for internal dashboards and complex data queries.
Practice Questions
1. What is the N+1 Problem in GraphQL and how do you solve it?
The N+1 Problem occurs when a resolver fetches related data for each item in a list individually, causing N additional queries. Solve it with DataLoader, which batches individual requests into a single query and caches results per request.
2. How does caching differ between REST and GraphQL?
REST caches at the HTTP level using URL-based cache keys. GraphQL POST requests all hit the same URL, so URL-based caching is impossible. GraphQL requires persisted queries (APQ), response caching at the resolver level, or CDN integration that understands GraphQL.
3. What is a common use case where REST is clearly better than GraphQL?
Public APIs served through CDNs. REST URLs are cacheable by every CDN and browser. GraphQL POST requests bypass most CDN caching layers unless persisted queries are used, which adds complexity.
4. How does GraphQL handle versioning differently from REST?
REST typically versions through the URL (/v1/users) or headers. GraphQL avoids versioning by deprecating fields — old fields remain but are marked @deprecated. The client chooses when to migrate, and the server never breaks existing queries.
Challenge
Design a hybrid API where: (1) public data (products, categories) is served via REST with CDN caching and Cache-Control headers, (2) admin dashboards and reporting use GraphQL for flexible data aggregation, (3) the authentication endpoints use REST for broad client compatibility, and (4) the mobile app uses GraphQL for the primary feed but falls back to REST for offline caching.
Mini Project: REST to GraphQL Migration
// Migration-layer.js
// Incremental REST-to-Graphql Migration with a compatibility layer
const Express = require('Express');
const { ApolloServer, gql } = require('apollo-server-Express');
const app = Express();
const PORT = 4000;
// Shared data source
const products = [
{ id: 'p1', name: 'Widget', price: 9.99, category: 'tools' },
{ id: 'p2', name: 'Gadget', price: 24.99, category: 'electronics' },
];
// REST endpoints (legacy)
app.get('/API/products', (req, res) => {
console.log('[REST] GET /API/products');
res.JSON(products);
});
app.get('/API/products/:id', (req, res) => {
const product = products.find(p => p.id === req.params.id);
console.log(`[REST] GET /API/products/${req.params.id}`);
res.JSON(product || { error: 'Not found' });
});
// Graphql schema (new)
const typeDefs = gql`
type Product { id: ID! name: String! price: Float! category: String! }
type Query { products: [Product!]! product(id: ID!): Product }
`;
const resolvers = {
Query: {
products: () => {
console.log('[Graphql] Query all products');
return products;
},
product: (_, { id }) => {
console.log(`[Graphql] Query product ${id}`);
return products.find(p => p.id === id);
},
},
};
const server = new ApolloServer({ typeDefs, resolvers });
server.applyMiddleware({ app, path: '/Graphql' });
app.listen(PORT, () => {
console.log(`REST: HTTP://localhost:${PORT}/API/products`);
console.log(`Graphql: HTTP://localhost:${PORT}${server.graphqlPath}`);
});
Expected behavior: The server runs both REST and GraphQL on the same port, sharing the same data source. Teams can migrate endpoint by endpoint: add GraphQL resolvers alongside existing REST routes, then switch clients gradually without downtime.
Congratulations on completing this GraphQL vs REST tutorial! Next, explore API Gateway patterns for routing and managing both API styles, then learn about caching strategies for optimizing REST and GraphQL performance.
- Practice daily — Convert a small REST API to GraphQL and compare response sizes for the same data
- Build a project — Build a hybrid API Gateway that routes REST calls to legacy services and GraphQL calls to new services
- Explore related topics — Check out Apollo Federation for combining multiple GraphQL services and GraphQL Mesh for connecting REST APIs to GraphQL
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro