GraphQL vs REST: Choosing the Right API Style
GraphQL isn't a REST replacement — it's an alternative with different trade-offs that matter depending on your client complexity, team size, and data shape.

The GraphQL vs. REST debate often generates more heat than light. Teams adopt GraphQL because it's trendy, then struggle with caching, authorization, and N+1 queries. Others dismiss it entirely and build REST endpoints with 15 query parameters to avoid over-fetching. The right choice depends on your specific constraints — client diversity, data relationships, and team expertise.
The Core Difference
REST organizes APIs around resources with fixed response shapes. GraphQL gives clients a query language to request exactly the data they need.
// REST — multiple requests, potentially over-fetching
// GET /api/users/123
// GET /api/users/123/orders?limit=5
// GET /api/users/123/reviews?limit=3
// Each endpoint returns its full schema, including fields the client doesn't use
// GraphQL — one request, exact data
const query = `
query UserDashboard($userId: ID!) {
user(id: $userId) {
name
avatar
orders(limit: 5) {
id
total
status
}
reviews(limit: 3) {
rating
comment
}
}
}
`;With REST, the server decides what data to return. With GraphQL, the client decides. This shifts complexity from the client to the server.
Where REST Wins
REST is simpler for straightforward CRUD operations with well-defined resources.
// ✅ REST excels at simple, resource-oriented APIs
// Clear, cacheable, easy to understand
app.get("/api/products/:id", getProduct);
app.post("/api/products", createProduct);
app.patch("/api/products/:id", updateProduct);
app.delete("/api/products/:id", deleteProduct);
// HTTP caching works out of the box
// GET /api/products/123
// Cache-Control: public, max-age=300
// ETag: "abc123"// ❌ GraphQL adds complexity for simple operations
const CREATE_PRODUCT = gql`
mutation CreateProduct($input: CreateProductInput!) {
createProduct(input: $input) {
id
name
price
}
}
`;
// Every request is POST, every response is 200
// HTTP caching doesn't work without additional infrastructure
// Error handling is an envelope format, not HTTP status codesREST advantages: HTTP caching, simpler tooling, status code semantics, smaller learning curve, CDN compatibility.
Where GraphQL Wins
GraphQL shines when clients have diverse data needs — especially mobile apps with bandwidth constraints or dashboards aggregating data from multiple domains.
// ❌ REST — the mobile app needs 5 requests for one screen
const user = await fetch("/api/users/me");
const orders = await fetch("/api/users/me/orders?limit=3");
const notifications = await fetch("/api/notifications?unread=true");
const recommendations = await fetch("/api/recommendations?limit=5");
const stats = await fetch("/api/users/me/stats");
// 5 round trips, over-fetching on each response
// ✅ GraphQL — one request, exact data needed
const DASHBOARD = gql`
query Dashboard {
me {
name
avatar
}
myOrders(limit: 3) {
id
total
status
}
notifications(filter: { unread: true }) {
id
message
}
recommendations(limit: 5) {
id
title
image
}
myStats {
totalOrders
totalSpent
}
}
`;One request replaces five. The client gets exactly the fields it needs, nothing more. On mobile networks, this matters.
The N+1 Problem in GraphQL
GraphQL's flexible queries create a server-side challenge: resolving nested fields can trigger hundreds of database queries.
// This innocent query triggers 1 + N database calls
// 1 query for users, then N queries for each user's orders
const query = `{
users(limit: 50) {
name
orders {
id
total
}
}
}`;// ✅ DataLoader batches and deduplicates database calls
import DataLoader from "dataloader";
const orderLoader = new DataLoader(async (userIds: string[]) => {
// One query for ALL user orders, not one per user
const orders = await db.orders.findMany({
where: { userId: { in: userIds } },
});
// Group by userId and return in the same order as input
const ordersByUser = new Map<string, Order[]>();
for (const order of orders) {
const list = ordersByUser.get(order.userId) ?? [];
list.push(order);
ordersByUser.set(order.userId, list);
}
return userIds.map((id) => ordersByUser.get(id) ?? []);
});
// In the resolver
const resolvers = {
User: {
orders: (user: User) => orderLoader.load(user.id),
},
};DataLoader is essential for any non-trivial GraphQL server. Without it, performance degrades exponentially with query depth.
Authorization Complexity
REST authorization is straightforward — middleware checks before the handler runs. GraphQL authorization must happen at the field level because different fields may have different access rules.
// REST — one auth check per endpoint
app.get("/api/users/:id", requireAuth, requireOwnerOrAdmin, getUser);
// GraphQL — auth at the field level
const resolvers = {
User: {
email: (user, args, context) => {
// Only the user themselves or an admin can see email
if (context.userId !== user.id && !context.isAdmin) return null;
return user.email;
},
salary: (user, args, context) => {
// Only HR or the user themselves
if (context.userId !== user.id && !context.roles.includes("hr")) {
throw new ForbiddenError("Cannot access salary");
}
return user.salary;
},
},
};This per-field authorization is more granular but harder to audit. "Who can see what?" becomes a question that requires tracing through resolver code rather than scanning route middleware.
Decision Framework
| Factor | Choose REST | Choose GraphQL |
|---|---|---|
| Client diversity | 1-2 clients with similar needs | Multiple clients with different data needs |
| Data shape | Flat, resource-oriented | Deeply nested, relational |
| Caching needs | HTTP caching important | Custom caching acceptable |
| Team size | Small team, simple API | Larger team, dedicated API layer |
| Real-time needs | SSE or WebSocket separately | Subscriptions built-in |
| API consumers | External developers | Internal clients you control |
Key Takeaways
- REST is simpler for resource-oriented CRUD with good HTTP caching support
- GraphQL reduces over-fetching when clients have diverse, nested data needs
- DataLoader is mandatory for any GraphQL server to prevent N+1 query problems
- GraphQL shifts complexity to the server — authorization, caching, and performance become harder
- Don't choose based on trends — choose based on your client diversity, data shape, and team expertise


