GraphQL vs REST: When to Use Which
REST's simplicity vs GraphQL's flexibility — over-fetching, under-fetching, the N+1 problem, and how most companies end up using both.
The GraphQL vs REST debate gets tribal fast. GraphQL fans act like REST is a relic. REST purists insist GraphQL is overengineered complexity. The reality is boring: they solve different problems, and most companies of any size use both.
REST: Simple, Predictable, Cacheable
REST maps HTTP methods to CRUD operations on resources. You know the pattern:
GET /api/users/42 → read user
POST /api/users → create user
PUT /api/users/42 → update user
DELETE /api/users/42 → delete user
GET /api/users/42/posts → user's posts
Each endpoint returns a fixed shape. You design the response once, clients consume it. HTTP caching works out of the box — CDNs, browser caches, and reverse proxies all understand GET requests with cache headers. Status codes have standard meanings. Every developer knows how REST works.
Here's a typical REST response:
// GET /api/users/42
{
"id": 42,
"name": "Alice Chen",
"email": "alice@example.com",
"avatar_url": "https://cdn.example.com/avatars/42.jpg",
"bio": "Software engineer at ...",
"created_at": "2024-01-15T08:30:00Z",
"followers_count": 1284,
"following_count": 312,
"posts_count": 87,
"location": "San Francisco, CA",
"website": "https://alicechen.dev"
}
If your mobile app only needs the name and avatar for a profile card, you're still downloading the bio, location, website, and everything else. That's over-fetching.
Now say your profile page needs the user data AND their last 5 posts AND their follower list. That's three separate HTTP requests:
GET /api/users/42
GET /api/users/42/posts?limit=5
GET /api/users/42/followers?limit=20
Three round trips. On a mobile connection with 200ms latency, that's 600ms of just waiting for responses. That's under-fetching — one endpoint doesn't give you enough, so you make multiple calls.
GraphQL: Ask for Exactly What You Need
GraphQL lets the client specify the exact shape of the response. One request, one response, exactly the fields you asked for:
query {
user(id: 42) {
name
avatarUrl
posts(limit: 5) {
title
createdAt
}
followers(limit: 20) {
name
avatarUrl
}
}
}
One HTTP request. The response mirrors the query structure. The mobile app asking for a profile card sends a different query than the desktop app rendering a full profile page. Same API, different data shapes.
The schema defines what's available:
type User {
id: ID!
name: String!
email: String!
avatarUrl: String
bio: String
posts(limit: Int): [Post!]!
followers(limit: Int): [User!]!
}
type Post {
id: ID!
title: String!
body: String!
createdAt: DateTime!
author: User!
comments: [Comment!]!
}
The schema is self-documenting. Tools like GraphiQL and Apollo Studio let developers explore the API interactively, see available fields, and test queries without reading separate documentation.
Mutations handle writes:mutation {
createPost(input: { title: "GraphQL vs REST", body: "..." }) {
id
title
createdAt
}
}
Subscriptions handle real-time data over WebSockets:
subscription {
newComment(postId: "123") {
body
author { name }
}
}
The N+1 Problem
Here's what GraphQL tutorials gloss over. Say a client queries:
query {
posts(limit: 20) {
title
author {
name
}
}
}
A naive resolver fetches 20 posts (1 query), then for each post fetches the author (20 queries). That's 21 database queries for one GraphQL request. This is the N+1 problem, and it will destroy your database performance.
The solution is DataLoader — a batching and caching utility that collects all the author IDs from those 20 posts and fetches them in a single query. Every serious GraphQL server uses it, but you have to set it up yourself. REST APIs don't have this problem because each endpoint controls its own query.
// DataLoader batches these into one SQL query
const authorLoader = new DataLoader(async (authorIds) => {
const authors = await db.users.findByIds(authorIds);
return authorIds.map(id => authors.find(a => a.id === id));
});
// Resolver uses the loader instead of direct DB call
const resolvers = {
Post: {
author: (post) => authorLoader.load(post.authorId),
},
};
When REST Wins
Simple CRUD applications. If your API is straightforward — create, read, update, delete resources — REST is simpler to build, test, and maintain. No schema definitions, no resolver layers, no DataLoader. Caching. REST's resource-based URLs play perfectly with HTTP caching.GET /api/posts/42 can be cached at every layer — CDN, reverse proxy, browser. GraphQL sends POST requests to a single endpoint, so standard HTTP caching doesn't work. You need application-level caching (Apollo Client, Relay) which is more complex.
File uploads. REST handles multipart file uploads natively. GraphQL requires workarounds or separate upload endpoints.
Team familiarity. Every backend developer knows REST. GraphQL has a learning curve — schema design, resolvers, DataLoader, query complexity limits. If your team is small and doesn't have GraphQL experience, the ramp-up cost is real.
When GraphQL Wins
Multiple client types. A mobile app, a web SPA, a partner API, and an internal dashboard all need different data from the same backend. With REST, you either create bespoke endpoints for each client or accept over-fetching. With GraphQL, each client queries exactly what it needs. Complex, nested data. Social networks, e-commerce product pages, dashboards pulling from multiple data sources — anything where a single view needs data from several related entities. One GraphQL query replaces five REST calls. Rapid frontend iteration. Frontend teams can change what data they fetch without backend changes. No waiting for a new endpoint or a modified response shape. This is a genuine productivity win in larger organizations where frontend and backend teams work asynchronously.The Pragmatic Take
Most companies I've seen end up with REST for simple services and GraphQL as a gateway layer that aggregates data from those REST services. GitHub's API v4 is GraphQL; their internal services still communicate over REST and gRPC. Shopify, Airbnb, and Netflix follow similar patterns.
Starting a new project? Default to REST. It's simpler, you'll ship faster, and you can always add GraphQL later when you hit the pain points it solves. If you're already dealing with over-fetching headaches, multiple clients needing different data shapes, or chatty API calls on mobile — that's when GraphQL starts paying for itself.
Both REST API design and GraphQL schemas are things you can practice on CodeUp with interactive exercises. Building a few endpoints in both styles makes the trade-offs obvious in a way that reading about them never does.
The best API is the one your team can build, maintain, and debug at 2 AM. Pick based on your actual problems, not hype.