GraphQL vs REST: Which API Style Should You Use?
Learn the real differences between GraphQL and REST APIs. When GraphQL solves real problems, when REST is the right choice, and how to decide for your project.
GraphQL came out of Facebook with big promises: no overfetching, no underfetching, one endpoint for everything. A decade later, most APIs are still REST.
Here's why — and when GraphQL actually wins.
The Problem GraphQL Solves
REST has a specific pain point: the shape of the response is decided by the server.
You have a user profile page. You need: user name, avatar, last 5 posts, and follower count.
In REST, you might hit:
GET /users/123 → name, email, bio, phone, address, preferences...
GET /users/123/posts → all posts with full content, comments, likes...
GET /users/123/followers → full follower objects...Three requests. Most of the data you don't need. This is overfetching (too much data) and underfetching (need multiple requests).
In GraphQL, you ask for exactly what you need:
query UserProfile($userId: ID!) {
user(id: $userId) {
name
avatar
posts(limit: 5) {
title
createdAt
}
followerCount
}
}One request. Exactly the fields you asked for.
How GraphQL Works
One endpoint (/graphql). Clients send queries (reads) or mutations (writes):
# Server: define schema
type_defs = """
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
}
type Query {
user(id: ID!): User
posts(limit: Int): [Post!]!
}
type Mutation {
createPost(title: String!, content: String!): Post!
}
"""
# Resolvers — functions that fetch the data
def resolve_user(obj, info, id):
return db.query("SELECT * FROM users WHERE id = %s", id)
def resolve_posts(user, info):
return db.query("SELECT * FROM posts WHERE user_id = %s", user.id)// Client: fetch exactly what you need
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
name
posts {
title
}
}
}
`;
const { data } = useQuery(GET_USER, { variables: { id: "123" } });Where GraphQL Wins
Multiple clients with different needs. Mobile app needs 3 fields. Web app needs 10. Admin panel needs 20. With REST, you either make a different endpoint for each or overfetch. With GraphQL, each client requests exactly what it needs.
Rapid frontend iteration. Backend schema doesn't change. Frontend devs add or remove fields from their queries without touching the backend. No more "can you add X field to the API?" tickets.
Deeply nested data. Social graphs, content hierarchies, anything where data is connected. REST requires multiple requests. GraphQL traverses the graph in one query.
API for third-party developers. GitHub's GraphQL API lets developers fetch exactly the data they need for their app. GitHub doesn't have to guess what each developer needs.
Where REST Wins
Simplicity. REST is easier to understand, debug, and build. HTTP status codes, standard verbs, curl-able URLs — every developer knows how to use it.
# REST — easy to debug
curl https://api.example.com/users/123
curl -X POST https://api.example.com/orders -d '{"items": [...]}'
# GraphQL — harder to curl, need to understand schema first
curl -X POST https://api.example.com/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ user(id: \"123\") { name email } }"}'Caching is straightforward. REST: GET /products/123 is cacheable by URL. CDN caches it automatically. GraphQL sends everything as POST — cache keys are complex, CDN caching doesn't work out of the box.
File uploads. REST: multipart/form-data upload to /upload. GraphQL: needs workarounds (GraphQL multipart spec or upload endpoint alongside GraphQL).
Simple CRUD APIs. If your API is create/read/update/delete on a few resources, REST is less overhead. GraphQL's schema definition + resolver setup is boilerplate you don't need.
Public APIs that humans debug. REST APIs are self-documenting via their URL structure. GraphQL requires a schema explorer.
The N+1 Problem
GraphQL's biggest pitfall: resolvers run per field, causing N+1 database queries.
# GraphQL resolvers
def resolve_posts(obj, info):
return db.query("SELECT * FROM posts") # 1 query: fetches 100 posts
def resolve_post_author(post, info):
return db.query("SELECT * FROM users WHERE id = %s", post.user_id)
# Runs 100 times — one per post = 101 queries totalFix with DataLoader — batches and deduplicates DB calls:
from strawberry.dataloader import DataLoader
async def load_users(user_ids: list[int]) -> list[User]:
# ONE query for all users
users = db.query("SELECT * FROM users WHERE id = ANY(%s)", user_ids)
return [users_by_id[uid] for uid in user_ids]
user_loader = DataLoader(load_fn=load_users)
def resolve_post_author(post, info):
return user_loader.load(post.user_id) # Batched, not individual queriesWithout DataLoader, GraphQL APIs can be slower than REST despite fetching less data.
Versioning
REST: version in the URL (/v1/users, /v2/users). Clean, explicit, easy to deprecate.
GraphQL: no versioning. You add fields and deprecate old ones in the same schema. Schema evolution instead of API versions.
type User {
name: String!
fullName: String! @deprecated(reason: "Use name instead")
avatar: String # New field, clients opt in
}GraphQL versioning is elegant in theory. In practice, keeping clients updated on deprecated fields requires tooling and discipline.
The Decision
| Factor | Choose REST | Choose GraphQL |
|---|---|---|
| API consumers | Few, known | Many, varied |
| Data shape | Same for all clients | Different per client |
| Caching | Critical | Less important |
| Team familiarity | Most teams | GraphQL-experienced team |
| Real-time | Polling / webhooks | Subscriptions |
| Public API | Yes | Depends |
Rule of thumb: REST for server-to-server APIs, internal microservices, and simple CRUD. GraphQL for client-facing APIs with diverse clients (mobile, web, third-party), complex data graphs, and when frontend iteration speed is critical.
Most companies start REST. Facebook, GitHub, Shopify, and Twitter added GraphQL after reaching scale with diverse client needs — not from day one.
Key Takeaways
- GraphQL: clients request exactly the data they need — solves overfetching and multiple requests
- REST: simpler, cacheable, familiar — right for most APIs
- GraphQL wins with multiple clients needing different data shapes, and rapid frontend iteration
- REST wins with simple CRUD, caching requirements, and public/debuggable APIs
- N+1 queries are GraphQL's main pitfall — always use DataLoader
- You can use both: REST for simple endpoints, GraphQL for complex data-fetching needs
- Don't adopt GraphQL for its own sake — adopt it when REST's shape-constraints become painful
Start REST. Add GraphQL when you have a concrete fetching problem it solves.
Related reading: REST API Design Best Practices · API Gateway Pattern · Rate Limiting Your API
Enjoyed this article?
Get weekly insights on backend architecture, system design, and Go programming.
Related Posts
Continue reading with these related posts
REST API Design Best Practices That Actually Matter
Learn REST API design best practices: URL structure, HTTP methods, status codes, versioning, and error handling. Build APIs developers love to use.
WebSockets vs SSE vs Long Polling: Real-Time Communication Explained
Learn how WebSockets, Server-Sent Events, and long polling work. When to use each for real-time features like chat, notifications, and live dashboards.
Idempotency in APIs: Preventing Duplicate Operations
Learn what idempotency means in API design and why it matters for payments, retries, and distributed systems. With practical implementation patterns.