The GraphQL vs REST debate has been running since Facebook open-sourced GraphQL in 2015. A decade later, both approaches are mature, widely adopted, and suited to different problems. The question is not which one is better. The question is which one fits your situation.
This guide compares GraphQL and REST across every dimension that matters in production: architecture, data fetching, performance, caching, versioning, error handling, and developer experience. Each section includes code examples so you can see the differences in practice.
Test queries interactively. The GraphQL Playground lets you write and test GraphQL queries against any endpoint directly in your browser.
Architecture Differences
REST organizes APIs around resources. Each resource has a URL, and HTTP methods define the operations: GET reads, POST creates, PUT/PATCH updates, DELETE removes. A user resource lives at /api/users/123, and you interact with it using standard HTTP semantics.
GraphQL organizes APIs around a type system and a query language. There is one endpoint (typically /graphql). The client sends a query describing exactly what data it wants, and the server returns that data in the same shape.
REST: Multiple Endpoints, Fixed Responses
GET /api/users/123
{
"id": 123,
"name": "Sarah Chen",
"email": "sarah@example.com",
"avatar": "https://...",
"role": "admin",
"department": "engineering",
"createdAt": "2025-03-15T10:00:00Z",
"lastLogin": "2026-02-14T08:30:00Z"
}
GET /api/users/123/orders?limit=5
{
"orders": [
{ "id": 456, "total": 99.99, "status": "shipped", "items": [...] },
...
]
}
GET /api/users/123/notifications?unread=true
{
"notifications": [...]
}
GraphQL: One Endpoint, Flexible Queries
POST /graphql
{
"query": "
query UserDashboard($id: ID!) {
user(id: $id) {
name
email
orders(limit: 5) {
id
total
status
}
notifications(unread: true) {
id
message
}
}
}
",
"variables": { "id": "123" }
}
The GraphQL query fetches everything in one request. The REST approach needed three separate requests. For a dashboard view that assembles data from multiple related resources, this difference compounds.
Data Fetching: Over-Fetching and Under-Fetching
Over-fetching happens when the API returns more data than the client needs. The REST response above includes avatar, role, department, createdAt, and lastLogin even if the client only needs the name and email. On a mobile connection, this wastes bandwidth and slows rendering.
Under-fetching happens when a single endpoint does not provide enough data, forcing additional requests. To display a user profile with their recent orders and notifications, the REST client made three sequential calls. Each round trip adds latency, especially on high-latency mobile networks.
GraphQL eliminates both problems by design. The client specifies exactly which fields to return, and nested relationships are resolved in one request.
REST APIs can use sparse fieldsets (?fields=name,email), compound documents, or BFF (Backend for Frontend) patterns to reduce over-fetching and under-fetching. GraphQL makes this the default behavior rather than an add-on.
Performance Comparison
| Dimension | REST | GraphQL |
|---|---|---|
| Network requests | Multiple endpoints, multiple round trips | Single request for complex data needs |
| Payload size | Fixed response, may include unused fields | Only requested fields returned |
| HTTP caching | Built-in via GET URLs, CDN-friendly | Requires custom caching layers |
| Server complexity | Simple query per endpoint | Resolver chain, potential N+1 queries |
| Parsing overhead | None (standard HTTP) | Query parsing and validation on every request |
| Bandwidth on mobile | Higher (over-fetching) | Lower (exact fields) |
The N+1 Problem in GraphQL
Consider a query that fetches a list of posts and each post's author. Without optimization, the resolver fetches 20 posts, then makes 20 separate database queries for each author. This is the N+1 problem.
// Problem: N+1 queries
const resolvers = {
Query: {
posts: () => db.posts.findMany({ limit: 20 }),
},
Post: {
// Called once per post = 20 additional queries
author: (post) => db.users.findById(post.authorId),
},
};
// Solution: DataLoader batches and deduplicates
import DataLoader from 'dataloader';
const userLoader = new DataLoader(async (ids) => {
const users = await db.users.findByIds(ids);
return ids.map(id => users.find(u => u.id === id));
});
const resolvers = {
Post: {
// DataLoader batches all 20 author IDs into one query
author: (post) => userLoader.load(post.authorId),
},
};
Use the API Tester to measure response times and payload sizes for both your REST endpoints and GraphQL queries side by side.
Caching Strategies
REST Caching
REST has a natural caching advantage because each resource has a unique URL. Standard HTTP caching works at every layer.
// Server sets caching headers
app.get('/api/users/:id', (req, res) => {
const user = await getUser(req.params.id);
res.set({
'Cache-Control': 'public, max-age=60, s-maxage=300',
'ETag': generateETag(user),
'Last-Modified': user.updatedAt.toUTCString(),
});
res.json(user);
});
// CDN caches based on URL + headers
// Browser caches based on Cache-Control
// Proxy caches based on ETag
GraphQL Caching
GraphQL uses POST requests to a single endpoint, so URL-based caching does not work by default. You need application-level caching.
// Client-side: Apollo Client normalized cache
import { InMemoryCache, ApolloClient } from '@apollo/client';
const client = new ApolloClient({
cache: new InMemoryCache({
typePolicies: {
User: {
keyFields: ['id'],
},
Order: {
keyFields: ['id'],
},
},
}),
});
// Apollo stores each object by type + ID
// Querying the same user from different queries
// returns the cached version automatically
// Server-side: Persisted queries for CDN caching
// Client sends a hash instead of the full query
GET /graphql?extensions={"persistedQuery":{"sha256Hash":"abc123"}}&variables={"id":"123"}
// Since it is a GET with a unique URL, CDNs can cache it
API Versioning
REST Versioning
REST APIs typically version using URL paths or headers. Each version is a separate set of endpoints with potentially different response shapes.
// URL versioning
GET /api/v1/users/123 // Returns { name, email }
GET /api/v2/users/123 // Returns { name, email, avatar, role }
// Header versioning
GET /api/users/123
Accept: application/vnd.myapp.v2+json
Maintaining multiple versions means running multiple code paths or entire separate deployments. Deprecating a version requires migrating all clients, which can take months for public APIs.
GraphQL: No Versioning Needed
GraphQL avoids versioning entirely. You add new fields to the schema, and existing queries continue to work because they only request the fields they know about. To remove a field, you deprecate it first.
type User {
id: ID!
name: String!
email: String!
avatar: String # Added later, old queries unaffected
role: String # Added later
username: String @deprecated(reason: "Use 'name' instead")
}
Clients that query for name and email continue to work unchanged when avatar and role are added. The @deprecated directive marks username in developer tools without breaking anything.
Error Handling
REST Errors
REST uses HTTP status codes, which are standardized and widely understood.
// 404 Not Found
{ "error": "User not found" }
// 400 Bad Request
{ "error": "Validation failed", "details": { "email": "Invalid format" } }
// 401 Unauthorized
{ "error": "Authentication required" }
// 500 Internal Server Error
{ "error": "Something went wrong" }
GraphQL Errors
GraphQL always returns HTTP 200, even when there are errors. Errors are reported in the response body alongside any partial data that could be resolved.
// GraphQL returns partial data + errors
{
"data": {
"user": {
"name": "Sarah Chen",
"orders": null
}
},
"errors": [
{
"message": "Failed to fetch orders",
"path": ["user", "orders"],
"extensions": {
"code": "DOWNSTREAM_SERVICE_ERROR",
"serviceName": "order-service"
}
}
]
}
This partial response model means the client can still render the user name even though orders failed. With REST, the entire request would have either succeeded or failed. GraphQL gives you more granular error handling at the cost of not being able to rely on HTTP status codes for monitoring.
Use the JSON Formatter to inspect and debug both REST response bodies and GraphQL error payloads with proper indentation and syntax highlighting.
Real-World Examples
E-Commerce Product Page
A product page needs the product details, reviews, related products, and the user's cart status.
// REST: 4 requests
GET /api/products/abc
GET /api/products/abc/reviews?limit=10
GET /api/products/abc/related?limit=4
GET /api/cart
// GraphQL: 1 request
query ProductPage($id: ID!) {
product(id: $id) {
name
price
description
images { url alt }
reviews(limit: 10) {
rating
text
author { name }
}
related(limit: 4) {
id
name
price
thumbnail
}
}
cart {
itemCount
total
}
}
Admin Dashboard vs Public API
An admin dashboard displays user lists with inline stats. A public API serves mobile apps with minimal data. GraphQL handles both from one schema. REST would need different endpoints or field filtering.
// Admin query: full data
query AdminUsers {
users(limit: 50) {
id name email role
stats { loginCount lastActive orderTotal }
flags { suspended verified }
}
}
// Mobile query: minimal data
query MobileUsers {
users(limit: 20) {
id name avatar
}
}
When to Choose REST
- Public APIs for third parties. REST is universally understood. Every language has an HTTP client. Documentation is straightforward with OpenAPI/Swagger.
- Simple CRUD applications. When each endpoint maps cleanly to a database table and clients need the full resource, REST is simpler.
- File uploads and downloads. Streaming binary data over HTTP is natural with REST. GraphQL requires multipart extensions.
- Heavy caching requirements. If your API serves mostly cacheable, read-heavy traffic, REST's built-in HTTP caching is hard to beat.
- Webhook integrations. External services send POST requests to a URL. REST endpoints handle this natively.
- Microservice-to-microservice communication. Internal services with fixed contracts benefit from REST's simplicity. gRPC is even better here.
Validate and test your REST endpoints with the API Request Builder, which supports all HTTP methods, custom headers, and request bodies.
When to Choose GraphQL
- Multiple client platforms. Web, mobile, watch, and TV apps all need different data shapes from the same backend.
- Complex, related data. Social graphs, nested resources, and dashboard views that pull from many sources.
- Rapid frontend iteration. Frontend teams can change their queries without waiting for backend API changes.
- API gateway over microservices. GraphQL unifies multiple backend services behind one schema that clients query.
- Real-time subscriptions. GraphQL subscriptions provide a clean model for push-based data updates over WebSockets.
- Avoiding version proliferation. Adding fields does not break existing clients, eliminating the need for v1/v2/v3 endpoints.
Migration Strategies
REST to GraphQL: Gateway Pattern
The lowest-risk approach is putting a GraphQL gateway in front of existing REST services. The gateway defines the schema and resolvers call the REST endpoints internally.
// GraphQL resolver calling existing REST API
const resolvers = {
Query: {
user: async (_, { id }) => {
const response = await fetch(`http://user-service/api/users/${id}`);
return response.json();
},
},
User: {
orders: async (parent) => {
const response = await fetch(
`http://order-service/api/users/${parent.id}/orders`
);
return response.json();
},
},
};
This lets you adopt GraphQL incrementally. Frontend teams get the query flexibility immediately while backend services continue running unchanged.
GraphQL to REST: Not Common, But Possible
If you need to expose a REST API from a GraphQL backend, tools like Sofa API can auto-generate REST endpoints from your GraphQL schema. This is useful when you need to provide a REST API for third-party integrations while keeping GraphQL internally.
Validate your API response schemas during migration with the JSON Schema Generator. It creates schemas from sample JSON payloads that you can use for automated validation.
Many production systems run GraphQL for client-facing queries and REST for webhooks, file handling, and third-party integrations. Choosing one does not mean abandoning the other.
Related Developer Tools
Free browser-based tools for API development and testing.
Frequently Asked Questions
The fundamental difference is how clients request data. REST exposes multiple endpoints, each returning a fixed data structure. The client calls GET /users/1 and receives whatever fields the server decided to include. GraphQL exposes a single endpoint where the client sends a query specifying exactly which fields it needs. The client asks for user(id: 1) { name, email } and receives only those two fields. This means REST can over-fetch (returning unused fields) or under-fetch (requiring multiple requests), while GraphQL returns precisely what was requested in a single round trip.
GraphQL is not inherently faster at the network or database level. Its performance advantage comes from reducing the number of round trips and the amount of data transferred. A mobile app that would need three REST calls to assemble a screen can make one GraphQL query. However, GraphQL can be slower if queries are deeply nested, triggering the N+1 problem on the server without proper use of DataLoader or batching. REST benefits from HTTP caching at every layer including CDNs, browser cache, and proxies which is harder to achieve with GraphQL since all requests go to a single POST endpoint. The faster approach depends on your access patterns, not the protocol itself.
Use REST when your API has simple, predictable access patterns where each endpoint maps cleanly to a resource. REST is the better choice for public APIs consumed by third parties because it is universally understood, for file uploads and downloads, for APIs that benefit heavily from HTTP caching at the CDN level, for microservices communicating internally where the data contract is fixed, and for teams without GraphQL experience where the learning curve would slow delivery. REST is also simpler to monitor, rate limit, and secure at the infrastructure level because each endpoint can be managed independently.
Use GraphQL when clients have diverse data needs. It excels when multiple client platforms like web, mobile, and watch apps need different subsets of the same data, when screens require data from multiple related resources in a single view, when you need to iterate quickly on the frontend without waiting for backend endpoint changes, when you want to avoid API versioning by adding fields without breaking existing queries, and when your data graph has complex relationships that would require many nested REST calls. GraphQL is particularly valuable for product teams where frontend developers need to move fast without being blocked by API changes.
REST has a built-in caching advantage because each resource has a unique URL. CDNs, browser caches, and HTTP proxies can cache GET /users/1 using standard HTTP headers like Cache-Control and ETag. GraphQL sends all requests as POST to a single endpoint, so URL-based HTTP caching does not apply out of the box. To cache GraphQL, you use client-side normalized caches like Apollo Client or urql that cache by object type and ID, persisted queries that map a hash to a query allowing GET requests with CDN caching, and server-side caching with Redis or in-memory stores keyed by query hash and variables. GraphQL caching requires more setup but offers finer-grained control over what gets cached and invalidated.
Yes, and many production systems do exactly this. A common pattern is using GraphQL as a gateway that sits in front of existing REST microservices. The GraphQL server defines the schema and resolvers, and each resolver calls the appropriate REST endpoint internally. This gives frontend teams the flexibility of GraphQL queries while backend teams continue maintaining their REST services. You can also expose both a REST API and a GraphQL API from the same server for different use cases: REST for simple CRUD and webhook integrations, GraphQL for complex client-facing queries. The key is choosing the right tool for each use case rather than forcing one approach everywhere.