GraphQL vs REST: Choosing the Right API Paradigm
Compare GraphQL and REST APIs, understand when to use each approach, schema design, queries, mutations, and trade-offs between the two paradigms.
GraphQL vs REST: Choosing the Right API Paradigm
GraphQL and REST are both approaches for building APIs, but they differ fundamentally in how clients interact with data. Understanding the trade-offs helps you choose the right approach for your project.
The RESTful API Design post covers REST principles. This post compares both approaches directly.
What is GraphQL?
GraphQL is a query language for APIs, developed by Facebook in 2012 and open-sourced in 2015. Instead of having multiple endpoints for different resources, you have a single endpoint that accepts queries.
graph LR
A[Client] -->|"POST /graphql"| B[GraphQL Server]
B --> C[Schema]
C --> D[Resolvers]
D --> E[Data Sources]
The client specifies exactly what data it needs in the query. The server returns exactly that data, nothing more.
Core Differences
REST: Multiple Endpoints
REST uses different endpoints for different resources:
# REST: Multiple endpoints
GET /users/123
GET /users/123/posts
GET /users/123/followers
Each endpoint returns a fixed data structure. If you need a user’s posts with follower counts, you might need multiple requests or get more data than necessary.
GraphQL: Single Endpoint
GraphQL uses a single endpoint with flexible queries:
# GraphQL: Single endpoint, flexible query
POST /graphql
query {
user(id: 123) {
name
posts {
title
}
followersCount
}
}
One request gets exactly the data you need.
Schema and Types
GraphQL uses a strongly-typed schema to define available data and operations:
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
followersCount: Int!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
comments: [Comment!]!
}
type Query {
user(id: ID!): User
posts(limit: Int): [Post!]!
}
type Mutation {
createPost(title: String!, content: String!): Post!
}
The schema serves as a contract between client and server. Clients know exactly what data is available.
REST and Schemas
REST does not enforce a schema. You document your API and clients hope for consistency. OpenAPI specification helps, but it is not enforced at runtime.
Queries
REST Queries
# REST: Get user and their posts
GET /users/123
GET /users/123/posts
GraphQL Queries
# GraphQL: One request, precise data
query GetUserWithPosts($userId: ID!) {
user(id: $userId) {
name
email
posts {
title
createdAt
}
}
}
GraphQL queries run in parallel automatically. If you request multiple fields, GraphQL fetches them concurrently.
Mutations
REST Mutations
# REST: Create, update, delete with different HTTP methods
POST /users
PUT /users/123
DELETE /users/123
GraphQL Mutations
# GraphQL: Mutations are explicit
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
updateUser(id: $id, input: $input) {
id
name
email
}
}
mutation DeleteUser($id: ID!) {
deleteUser(id: $id)
}
Data Fetching
Overfetching and Underfetching
REST often leads to overfetching (getting more data than needed) or underfetching (needing multiple requests):
# REST: Gets more data than needed
GET /users/123
# Returns: { id, name, email, created_at, updated_at, profile_url, bio, ... }
# REST: Multiple requests for related data
GET /users/123 # User info
GET /users/123/posts # User's posts
GET /posts/456/comments # Comments for a specific post
GraphQL solves both problems:
# GraphQL: Exact data
query {
user(id: 123) {
name # Only what you need
posts {
title # Only what you need
}
}
}
N+1 Problem
GraphQL can suffer from the N+1 problem: fetching a list of users, then making a separate request for each user’s posts:
# This could trigger many database queries
query {
users {
name
posts {
title # Triggers query for each user's posts
}
}
}
DataLoader solves this by batching requests.
Error Handling
REST Errors
REST uses HTTP status codes:
HTTP/1.1 404 Not Found
Content-Type: application/json
{"error": "User not found"}
GraphQL Errors
GraphQL returns 200 OK even for errors. Errors are in the response body:
{
"data": null,
"errors": [
{
"message": "User not found",
"locations": [{ "line": 3, "column": 5 }],
"path": ["user"]
}
]
}
This is controversial. Some prefer HTTP status codes for errors.
Caching
REST Caching
REST works well with HTTP caching:
GET /users/123
Cache-Control: max-age=3600
ETag: "v1"
CDNs, browser caches, and libraries like React Query handle REST caching well.
GraphQL Caching
GraphQL POST requests are harder to cache by default. Solutions:
- Normalized caching with Apollo Client or Relay
- Persisted queries that become GET requests
- Response caching at the CDN level
When to Choose REST
REST works well when:
- Your API is simple with predictable data requirements
- You need HTTP caching
- You are building public APIs consumed by many clients
- Your team is familiar with REST
- You need simple documentation (just list endpoints)
Examples: CRUD applications, simple CRUD APIs, public APIs for third-party developers
When to Choose GraphQL
GraphQL works well when:
- Clients need different data shapes
- Mobile apps needing minimal data transfer
- Complex domains with many related entities
- Rapid iteration with frontend teams
- You want strong typing and schema validation
Examples: Mobile apps, complex dashboards, microservices with varying client needs
Comparison Table
| Aspect | REST | GraphQL |
|---|---|---|
| Data fetching | Multiple requests | Single request |
| Data shape | Fixed per endpoint | Client specifies |
| Typing | Documentation | Schema enforced |
| Caching | HTTP caching | Custom caching |
| Learning curve | Lower | Higher |
| Tooling | Mature | Evolving |
| Error handling | HTTP status codes | 200 + error body |
| Overfetching | Common | Avoided |
Combining Both
You do not have to choose one exclusively. Some teams use REST for simple operations and GraphQL for complex data requirements.
# REST for simple operations
GET /health
GET /config
# GraphQL for complex data
POST /graphql
Production Failure Scenarios
| Failure | Impact | Mitigation |
|---|---|---|
| Query complexity explosion | Server overwhelmed; possible DoS | Implement query depth limiting; set complexity budgets |
| N+1 query problem | Database flooded with queries; slow responses | Use DataLoader for batching; optimize resolvers |
| Schema introspection abuse | Information leakage; enumeration attacks | Disable introspection in production; restrict access |
| Subscription memory leaks | Server memory grows; eventual crash | Set subscription limits; implement timeouts |
| Mutation race conditions | Data inconsistency between related operations | Implement optimistic locking; use transactions |
| Persisted query abuse | Attackers pre-store malicious queries | Validate persisted query hashes; rate limit |
| Error masking with 200 OK | Errors hidden; debugging difficult | Return proper error status codes; use extensions |
Observability Checklist
Metrics
- Query rate by operation type (query/mutation/subscription)
- Query complexity distribution
- Request duration by operation and field
- Error rate by error type
- DataLoader batch efficiency (cache hit ratio)
- Schema introspection requests
- Subscription active count
- Query depth and breadth distribution
Logs
- Query requests with variables and complexity
- Mutation requests with authorization context
- DataLoader batch operations and cache misses
- Error responses with path and locations
- Schema change events
- Subscription lifecycle events
- Security events (introspection attempts, rate limit hits)
Alerts
- Query complexity exceeds threshold
- Error rate exceeds normal baseline
- DataLoader cache hit ratio drops below 80%
- Subscription count exceeds limits
- Unusual introspection activity
- Query depth spikes indicate potential attack
Security Checklist
- Disable schema introspection in production
- Implement query complexity limits and depth limits
- Use persisted queries to prevent abuse
- Validate and sanitize all variable inputs
- Implement proper authorization at resolver level
- Log and monitor unusual query patterns
- Rate limit queries per client
- Protect against query batching attacks (array literals)
- Use query whitelisting for sensitive operations
- Validate that mutations affect only intended fields
- Implement request timeout at GraphQL layer
- Do not expose internal error details in responses
Common Pitfalls / Anti-Patterns
Overusing GraphQL for Simple Cases
GraphQL adds complexity. Simple REST endpoints may be better.
# Overkill: GraphQL for simple, predictable data
query {
healthCheck {
status
}
}
# Better: Simple REST endpoint
GET /health
Ignoring N+1 Queries
GraphQL makes N+1 problems easy to create.
# Problem: Fetches posts for each user separately
query {
users {
name
posts {
title
} # N queries for N users
}
}
# Better: Use DataLoader to batch
query {
users {
name
posts {
title
} # Single batched query
}
}
Not Implementing Proper Error Handling
GraphQL returns 200 OK even for errors.
// Problem: Error masked as success
{
"data": { "user": null },
"errors": [{ "message": "Not authorized" }]
}
// Better: Use proper HTTP status codes
{
"errors": [{
"extensions": { "code": "UNAUTHORIZED" }
}]
}
Exposing Schema Internals
Introspection can reveal your entire schema.
// Disable introspection in production
const server = new ApolloServer({
schema,
introspection: false, // Production
playground: false, // Production
});
Quick Recap
Key Bullets
- REST uses fixed endpoints returning predictable data; GraphQL uses single endpoint with flexible queries
- GraphQL eliminates overfetching and underfetching but adds complexity
- GraphQL schemas provide type safety and self-documenting APIs
- REST works well with HTTP caching; GraphQL requires custom caching strategies
- GraphQL subscriptions enable real-time updates
- N+1 queries are a common GraphQL performance issue; DataLoader solves this
- Both can coexist; use REST for simple cases, GraphQL for complex data requirements
- Error handling differs: REST uses HTTP status codes, GraphQL uses 200 with error bodies
Copy/Paste Checklist
# Test GraphQL query with curl
curl -X POST https://api.example.com/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{ user(id: 1) { name email } }"}'
# Test GraphQL mutation
curl -X POST https://api.example.com/graphql \
-H "Content-Type: application/json" \
-d '{"query":"mutation { createUser(input: {name: \"Alice\"}) { id } }"}'
# Introspection query (disable in production!)
curl -X POST https://api.example.com/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{ __schema { types { name } } }"}'
# Check query complexity
# Use Apollo Studio or GraphQL Playground analysis tools
Conclusion
REST and GraphQL represent different philosophies. REST is resource-oriented with fixed endpoints. GraphQL is query-oriented with flexible data fetching.
REST shines for simple, predictable APIs where caching matters. GraphQL shines for complex, client-driven data requirements where you want to avoid overfetching. Neither is universally better. Choose based on your specific needs.
For REST API design, see the RESTful API Design post. For versioning both types of APIs, see the API Versioning Strategies post.
Category
Related Posts
RESTful API Design: Best Practices for Building Web APIs
Learn REST principles, resource naming, HTTP methods, status codes, and best practices. Design clean, maintainable, and scalable RESTful APIs.
API Versioning Strategies: Managing API Evolution Without Breaking Clients
Learn different API versioning strategies including URL versioning, header versioning, and query parameters. Understand backward compatibility and deprecation practices.
Rate Limiting: Token Bucket, Sliding Window, and Distributed
Rate limiting protects APIs from abuse. Learn token bucket, sliding window, fixed window algorithms and distributed rate limiting at scale.