GraphQL and REST are the two dominant API paradigms today. REST has been the default for 15+ years, powering virtually every API you've used. GraphQL emerged from Facebook in 2015 promising to solve REST's over-fetching and under-fetching problems. In 2025, both are mainstream — and the choice genuinely matters for your project's long-term developer experience.
This guide cuts through the hype to give you a practical, honest comparison.
At a glance
| REST | GraphQL | |
|---|---|---|
| Created by | Roy Fielding, 2000 | Facebook (Meta), 2015 |
| Architecture | Resource-based endpoints | Single endpoint + query language |
| Data fetching | Fixed response shape | Client specifies exact fields |
| Over-fetching | Common problem | Eliminated by design |
| Under-fetching | Common (N+1 round trips) | Solved with nested queries |
| Caching | Native HTTP caching (CDN, browser) | Complex — requires extra tooling |
| Learning curve | Low | Moderate |
| Tooling maturity | Very mature | Mature (Apollo, urql, Relay) |
| Type system | Optional (OpenAPI) | Built-in (SDL) |
| Best for | Public APIs, simple CRUD, file uploads | Complex data graphs, mobile apps, BFF layer |
What is REST?
REST (Representational State Transfer) is an architectural style, not a protocol. It uses HTTP methods (GET, POST, PUT, PATCH, DELETE) on resource URLs. Each resource has its own endpoint.
GET /users/42 → fetch user
GET /users/42/posts → fetch user's posts
POST /users → create user
PUT /users/42 → replace user
DELETE /users/42 → delete user
A typical REST response:
GET /users/42
{
"id": 42,
"name": "Alice",
"email": "alice@example.com",
"createdAt": "2024-01-15",
"role": "admin",
"preferences": { "theme": "dark", "notifications": true },
"billingAddress": { ... }
}
You got everything — even if the client only needed name and email. That's over-fetching.
What is GraphQL?
GraphQL is a query language for your API. The client describes exactly what data it needs in a typed query. There is one endpoint (usually /graphql) and the server returns exactly what was requested.
query {
user(id: 42) {
name
email
}
}
Response:
{
"data": {
"user": {
"name": "Alice",
"email": "alice@example.com"
}
}
}
No extra fields. No wasted bandwidth. And you can fetch related data in one request:
query {
user(id: 42) {
name
posts(last: 5) {
title
publishedAt
comments { text author { name } }
}
}
}
That would be 3+ REST round trips — /users/42, /users/42/posts, /posts/:id/comments.
Core differences
1. Over-fetching and under-fetching
REST problem:
GET /users/42 → returns 20 fields, client needs 3 (over-fetch)
GET /users/42/posts → separate round trip (under-fetch)
GraphQL solution:
query { user(id: 42) { name posts { title } } }
One request, exact fields. Critical for mobile clients on slow connections.
2. Type system
GraphQL has a built-in Schema Definition Language (SDL) that acts as a contract:
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
}
type Post {
id: ID!
title: String!
body: String
author: User!
publishedAt: String
}
type Query {
user(id: ID!): User
posts(authorId: ID, limit: Int = 10): [Post!]!
}
type Mutation {
createPost(title: String!, body: String!): Post!
deletePost(id: ID!): Boolean!
}
REST relies on external docs (OpenAPI/Swagger), which can drift from reality. GraphQL's schema is the source of truth — you can introspect it at runtime.
3. Versioning
REST typically uses URL versioning:
/v1/users
/v2/users ← breaking change
/v3/users ← another breaking change
GraphQL avoids versioning by making fields nullable and using @deprecated:
type User {
name: String!
fullName: String @deprecated(reason: "Use name instead")
}
Clients that still use fullName continue working. New clients use name. No version bump needed.
4. Caching
This is GraphQL's biggest weakness.
REST maps naturally to HTTP caching:
GET /users/42 → CDN caches response
ETag, Last-Modified → conditional requests
Cache-Control → browser caches
GraphQL sends everything as POST by default:
- POST requests are not cached by HTTP intermediaries
- Apollo Client has its own normalized cache (by type + ID)
- Requires
persisted queriesto enable CDN caching - Apollo Federation has CDN caching support
For public APIs with heavy read traffic, REST + CDN is significantly cheaper and faster.
5. Error handling
REST uses HTTP status codes:
200 OK, 201 Created, 400 Bad Request, 401 Unauthorized, 404 Not Found, 500 Server Error
GraphQL always returns 200 OK — errors appear in the response body:
{
"data": { "user": null },
"errors": [
{
"message": "User not found",
"locations": [{ "line": 2, "column": 3 }],
"path": ["user"],
"extensions": { "code": "NOT_FOUND" }
}
]
}
This breaks HTTP monitoring tools, error trackers, and APM solutions by default — you need custom error parsing.
Performance comparison
| Scenario | REST | GraphQL |
|---|---|---|
| Simple CRUD (single resource) | ✅ Faster (less parsing overhead) | ⚠ Slight overhead |
| Complex nested data (3+ resources) | ❌ Multiple round trips | ✅ Single query |
| Mobile on slow network | ❌ Over-fetching wastes bandwidth | ✅ Exact fields |
| CDN caching | ✅ Native HTTP caching | ❌ Requires persisted queries |
| File uploads | ✅ Native multipart | ⚠ Needs graphql-upload |
| Real-time | ⚠ Polling or webhooks | ✅ Native subscriptions |
| Large response (paginated list) | ✅ Predictable | ⚠ Resolver complexity |
| High-concurrency public API | ✅ CDN absorbs traffic | ❌ CDN harder to leverage |
Developer experience
REST developer experience
Pros:
- Works in every HTTP client (curl, Postman, Insomnia, browser)
- Built-in HTTP semantics (status codes, headers, caching)
- Easy to test manually
- Simple to implement
- Universally understood
Cons:
- API docs can drift from implementation
- Client must know all endpoint URLs
- Versioning becomes messy at scale
- Over/under-fetching requires custom endpoints (leads to BFF explosion)
GraphQL developer experience
Pros:
- Schema is self-documenting and introspectable
- GraphiQL / Apollo Studio for interactive exploration
- Code generation (TypeScript types from schema)
- Single endpoint simplifies auth middleware
- Strongly typed — IDE autocomplete works perfectly
Cons:
- Resolver N+1 problem (requires DataLoader)
- Schema design complexity
- Monitoring harder (all 200 OK)
- Caching setup non-trivial
- Overkill for simple APIs
Tooling ecosystem
| Category | REST | GraphQL |
|---|---|---|
| API design | OpenAPI/Swagger, Stoplight | SDL, Apollo Studio |
| Client (JS) | Fetch, Axios, ky, SWR | Apollo Client, urql, Relay, TanStack Query |
| Client (mobile) | Retrofit (Android), URLSession (iOS) | Apollo iOS, Apollo Android |
| Testing | Postman, Insomnia, REST Assured | Apollo Client testing, graphql-request |
| Mocking | WireMock, MSW | MSW, GraphQL Faker |
| Gateway | Kong, AWS API GW, Nginx | Apollo Gateway, StepZen, Hasura |
| Monitoring | APM tools (Datadog, New Relic) | Apollo Studio, GraphQL Shield |
| Code gen | openapi-generator, orval | graphql-codegen |
| Subscriptions | SSE, WebSockets (manual) | Built-in subscriptions |
Security considerations
Both REST and GraphQL have security concerns, but GraphQL introduces some unique ones:
GraphQL-specific risks:
# Introspection — exposes schema to attackers in production
query { __schema { types { name fields { name } } } }
# Deep query attack — can overload resolvers
query {
users {
friends {
friends {
friends { friends { friends { name } } }
}
}
}
}
# Batch query abuse
query {
a: user(id: 1) { email }
b: user(id: 2) { email }
# ... repeat 1000 times
}
Mitigations:
| Threat | Solution |
|---|---|
| Introspection leak | Disable in production (introspection: false) |
| Deep query attack | Depth limiting (e.g., graphql-depth-limit) |
| Query complexity | Complexity analysis (e.g., graphql-cost-analysis) |
| Batch abuse | Rate limiting per client |
| Auth bypass | Field-level authorization (e.g., graphql-shield) |
REST has the same threats (unauthorized access, injection) but no GraphQL-specific attack surface.
The N+1 problem (GraphQL's Achilles heel)
The most common GraphQL performance pitfall:
query {
posts { # 1 query to fetch all posts
author { # N queries — one per post to fetch author!
name
}
}
}
If there are 100 posts, that's 101 database queries.
Solution: DataLoader (batching + caching):
const authorLoader = new DataLoader(async (authorIds) => {
// ONE query for all IDs
const authors = await db.select('users').whereIn('id', authorIds);
return authorIds.map(id => authors.find(a => a.id === id));
});
// Resolver
const resolvers = {
Post: {
author: (post) => authorLoader.load(post.authorId)
// ↑ Batched — all post.authorIds resolved in 1 DB query
}
};
REST can also have N+1 problems, but they're less common because endpoints are explicit.
When to use REST
| Scenario | Why REST wins |
|---|---|
| Public API | Universally understood, works with any client |
| File uploads/downloads | Native HTTP multipart/streaming |
| CDN-heavy workloads | Native HTTP caching |
| Simple CRUD | Less overhead, simpler implementation |
| Microservices (internal) | Stateless, easy load balancing |
| Streaming large datasets | HTTP chunked transfer encoding |
| Webhooks / callbacks | REST is the standard |
| API-first companies | Easier to document and onboard third parties |
When to use GraphQL
| Scenario | Why GraphQL wins |
|---|---|
| Complex data graphs | Multiple related types queried together |
| Mobile clients | Bandwidth-sensitive, exact data |
| Multiple client types | Web, iOS, Android all need different shapes |
| BFF (Backend for Frontend) | One GraphQL layer, different queries per client |
| Rapid prototyping | Schema evolves without versioning |
| Real-time apps | Native subscriptions via WebSocket |
| Internal developer tools | Self-documenting schema, no doc drift |
| Meta-level queries | Introspection, analytics, federation |
Migrating from REST to GraphQL
You don't have to pick one. Many teams run REST and GraphQL side by side:
Mobile App ──────┐
├──► GraphQL API ──► Microservices (REST)
Web Dashboard ───┘
↑
BFF Layer: GraphQL aggregates internal REST APIs
This pattern (GraphQL as a BFF aggregating REST microservices) is very common at scale.
Migration strategy:
- Keep existing REST endpoints (don't break existing clients)
- Add
/graphqlendpoint alongside - Migrate new features to GraphQL
- Gradually migrate existing clients as they need richer data
Full comparison
| Feature | REST | GraphQL |
|---|---|---|
| API style | Resource-based | Query language |
| Endpoints | Multiple (one per resource) | Single (/graphql) |
| HTTP methods | GET/POST/PUT/PATCH/DELETE | POST (or GET for queries) |
| Response shape | Fixed by server | Defined by client |
| Over-fetching | Common | Eliminated |
| Under-fetching | Common | Eliminated |
| Type system | Optional (OpenAPI) | Built-in (SDL) |
| Versioning | URL or header versioning | Schema evolution (@deprecated) |
| HTTP caching | Native (GET, ETags) | Complex (persisted queries) |
| Real-time | SSE, WebSocket (manual) | Subscriptions (built-in) |
| Error handling | HTTP status codes | errors array in 200 response |
| File uploads | Native multipart | Needs extra lib |
| N+1 problem | Less common | Common (solved with DataLoader) |
| Introspection | Via OpenAPI spec | Built-in at runtime |
| Code generation | openapi-generator | graphql-codegen |
| Security model | Standard HTTP | Additional attack surface |
| Learning curve | Low | Moderate |
| Best for | Public APIs, CRUD, CDN | Complex graphs, mobile, BFF |
GraphQL vs REST vs gRPC
| REST | GraphQL | gRPC | |
|---|---|---|---|
| Protocol | HTTP/1.1+ | HTTP/1.1+ | HTTP/2 |
| Data format | JSON (usually) | JSON | Protocol Buffers |
| Schema | OpenAPI (optional) | SDL (built-in) | .proto files |
| Streaming | SSE/WebSocket | Subscriptions | Native bidirectional |
| Performance | Good | Good | Excellent (binary) |
| Browser support | ✅ Native | ✅ Native | ⚠ Needs grpc-web |
| Human readable | ✅ Yes | ✅ Yes | ❌ Binary |
| Best for | Public APIs | Complex data | Internal microservices |
gRPC is the third option often overlooked — excellent for internal service-to-service communication where performance is critical.
Common mistakes
| Mistake | Problem | Fix |
|---|---|---|
| Using GraphQL for a simple 5-endpoint CRUD app | Overkill — adds complexity without benefit | Use REST |
| Disabling introspection in development | Breaks GraphiQL, code generation | Only disable in production |
| Not using DataLoader | N+1 DB queries, catastrophic performance | Use DataLoader for all list resolvers |
| Ignoring query complexity limits | Attackers can crash your server | Add depth + complexity limits |
| Using HTTP status 200 for GraphQL errors without error handling | Monitoring tools miss errors | Parse errors array in every response |
| Designing REST endpoints that over-fetch | REST becomes RESTful in name only | Consider GraphQL or sparse fieldsets |
| Running REST + GraphQL without a plan | Schema drift, duplicated logic | Pick a clear integration boundary |
| Over-resolving (returning full objects from mutations) | Bandwidth waste | Return only changed/needed fields |
FAQ
Is GraphQL faster than REST?
It depends on the use case. For simple single-resource fetches, REST is slightly faster (less parsing overhead). For complex multi-resource queries, GraphQL wins by eliminating round trips. REST with CDN caching beats GraphQL for highly cacheable read-heavy workloads.
Can I use GraphQL with any backend language?
Yes. There are GraphQL libraries for Node.js (Apollo, Yoga), Python (Ariadne, Strawberry), Go (gqlgen), Java (DGS Framework), Rust (async-graphql), Ruby (graphql-ruby), PHP (Lighthouse for Laravel), and more.
Does GraphQL replace REST?
No. REST remains the standard for public APIs, file operations, webhooks, and simple CRUD. GraphQL is an addition to your toolkit, not a replacement. Many successful architectures use REST for public-facing APIs and GraphQL internally.
What about GraphQL subscriptions vs WebSockets?
GraphQL subscriptions run over WebSockets (or SSE) — they're not a separate technology. You still need a WebSocket server. Apollo Server handles this automatically; other implementations vary.
Is REST dead?
No. REST powers the vast majority of APIs in production today. GitHub, Stripe, Twilio, and most cloud providers offer REST APIs as their primary interface. GraphQL adoption is growing but REST isn't going anywhere.
Should beginners learn REST or GraphQL first?
Learn REST first. It teaches fundamental HTTP concepts (methods, status codes, headers, caching) that apply everywhere. Once you're comfortable with REST, learning GraphQL takes a few days — the concepts build on each other.