GraphQL was supposed to kill REST. That was the narrative in 2016 when Facebook open-sourced it. Nearly a decade later, REST is alive and well—and so is GraphQL. Both are used extensively in production at scale.
The question isn't which one is better. The question is which one is right for your specific situation. This guide gives you the framework to answer that question, with concrete examples and honest tradeoffs.
The Core Problem Each Solves
Before comparing them, you need to understand the problem each was designed to solve.
REST was designed around resources and HTTP semantics. It's a set of conventions for how to structure URLs, HTTP methods, and responses. Its strength is simplicity and the web's existing infrastructure.
GraphQL was designed around the client's data requirements. It was created specifically to solve a problem Facebook had: mobile apps fetching too much data or making too many round trips. Its strength is precise data fetching.
How REST Works
A REST API exposes resources via URLs. Clients use HTTP methods to operate on those resources.
GET /users → list users
GET /users/42 → get user with ID 42
POST /users → create a new user
PUT /users/42 → replace user 42
PATCH /users/42 → partially update user 42
DELETE /users/42 → delete user 42
A typical REST response:
GET /users/42
{
"id": 42,
"name": "Alice Chen",
"email": "alice@example.com",
"role": "admin",
"createdAt": "2025-01-15T10:30:00Z",
"preferences": {
"theme": "dark",
"notifications": true
}
}
Multiple related resources require multiple requests:
GET /users/42 → user data
GET /users/42/posts → user's posts
GET /users/42/followers → user's followers
How GraphQL Works
A GraphQL API exposes a single endpoint. Clients send queries describing exactly what data they need.
# Client sends this query
query GetUserProfile($userId: ID!) {
user(id: $userId) {
id
name
email
posts(last: 5) {
title
publishedAt
}
followerCount
}
}
The server returns exactly that structure—no more, no less:
{
"data": {
"user": {
"id": "42",
"name": "Alice Chen",
"email": "alice@example.com",
"posts": [
{ "title": "My First Post", "publishedAt": "2026-01-10" },
{ "title": "GraphQL Deep Dive", "publishedAt": "2026-02-20" }
],
"followerCount": 847
}
}
}
The same query format is used for mutations (writes):
mutation UpdateUser($id: ID!, $name: String!) {
updateUser(id: $id, name: $name) {
id
name
updatedAt
}
}
And subscriptions (real-time):
subscription OnNewMessage($channelId: ID!) {
messageAdded(channelId: $channelId) {
id
content
author {
name
}
}
}
Key Differences
1. Over-fetching and Under-fetching
REST's challenge: Endpoints return fixed shapes. You either get too much data or too little.
GET /users/42
→ Returns all 20 fields, but you only need name and avatar
GET /posts/100
→ Returns post data, but not the author name—you need a second request
GraphQL's answer: Request exactly the fields you need, from any depth, in one query.
query {
post(id: 100) {
title
author { name, avatar }
}
}
This is particularly valuable for mobile apps where bandwidth matters.
2. Number of Requests
REST: Related data often requires multiple round trips.
Fetch user → GET /users/42
Fetch their org → GET /organizations/7
Fetch org teams → GET /organizations/7/teams
GraphQL: One query, one round trip.
query {
user(id: 42) {
name
organization {
name
teams {
name
memberCount
}
}
}
}
3. Type System and Schema
GraphQL has a strongly typed schema that serves as a contract between client and server.
type User {
id: ID!
name: String!
email: String!
role: UserRole!
posts: [Post!]!
createdAt: DateTime!
}
enum UserRole {
ADMIN
EDITOR
VIEWER
}
type Post {
id: ID!
title: String!
author: User!
publishedAt: DateTime
content: String!
}
The schema is introspectable—clients can query it to discover what's available. This powers tools like GraphiQL (interactive query explorer) and code generators that create typed client SDKs.
REST APIs can achieve this with OpenAPI/Swagger, but it's optional and often out of sync with the actual implementation.
4. Versioning
REST typically versions through URL (/v1/users, /v2/users) or headers (Accept: application/vnd.api+json;version=2).
GraphQL avoids versioning by design. You add new fields (they're ignored by old clients) and deprecate old ones:
type User {
id: ID!
name: String!
fullName: String! @deprecated(reason: "Use `name` instead")
email: String!
}
This is theoretically cleaner but requires discipline in practice.
5. Caching
REST is HTTP-native. GET requests are cached by browsers, CDNs, and proxies automatically. No configuration needed.
GraphQL typically uses POST for all queries (bypassing HTTP caching) or GET with query strings (complex, URL length limits). Proper caching requires client-side solutions like Apollo Client's normalized cache or persisted queries.
This is a significant operational difference. A high-traffic public API served by a CDN benefits enormously from REST's native HTTP caching.
6. File Uploads
REST: Straightforward multipart form data.
const form = new FormData();
form.append('file', fileInput.files[0]);
await fetch('/uploads', { method: 'POST', body: form });
GraphQL: Not specified in the spec. Requires the graphql-multipart-request-spec extension, which varies in support across clients and servers.
When to Choose REST
REST is the right choice when:
You're building a public API
Public APIs benefit from REST's simplicity, HTTP semantics, and tooling ecosystem. Developers integrating your API know REST. curl, Postman, HTTPie—all work naturally. Documentation is straightforward.
# This just works for REST
curl -H "Authorization: Bearer $TOKEN" https://api.example.com/v1/users
GraphQL's query language is a learning curve for API consumers.
Caching is critical
If you're building something like a product catalog, news articles, or any high-read, public-facing API, HTTP caching is a major advantage. REST GET endpoints can be cached at every layer: browser, CDN, reverse proxy.
Your data model is simple and resource-based
If your API maps cleanly to a set of resources with CRUD operations, REST fits naturally. A file storage API, a simple user management API, a payment API—these don't need GraphQL's flexibility.
Your team isn't familiar with GraphQL
The operational overhead of GraphQL is real: schema management, resolver architecture, performance monitoring (the N+1 problem), caching strategies. If your team doesn't have GraphQL experience, REST's simplicity has real value.
You need reliable HTTP tooling
Load balancers, API gateways, monitoring tools, logging infrastructure—all of these are built around HTTP semantics. REST plays nicely with everything.
When to Choose GraphQL
GraphQL is the right choice when:
You have multiple different clients with different data needs
A mobile app needs minimal data (bandwidth). A dashboard needs aggregated data. An admin panel needs everything. With REST, you either build multiple endpoint variants or all clients accept over-fetching.
With GraphQL, each client requests exactly what it needs from one schema.
You're building a frontend-heavy product with complex data requirements
Product pages that aggregate user data, order history, recommendations, and inventory in one view benefit from GraphQL's ability to express those relationships in a single query.
Your API is primarily consumed by your own frontend teams
GraphQL shines in an internal BFF (Backend for Frontend) context. The strong type system and code generation (GraphQL Codegen, Relay) give frontend teams a typed, self-documenting API.
# Generate TypeScript types from your schema
graphql-codegen --config codegen.yml
Frontend developers can explore available data with GraphiQL, generate typed hooks, and get compile-time errors when queries don't match the schema.
You're dealing with the N+1 problem through DataLoader
The N+1 problem is real in GraphQL (fetching a list of posts, then N requests for each author). But DataLoader solves it elegantly by batching and caching requests:
const userLoader = new DataLoader(async (ids) => {
const users = await db.users.findMany({ where: { id: { in: ids } } });
return ids.map(id => users.find(u => u.id === id));
});
In REST, you'd solve this with compound endpoints or eager loading—both involve tight coupling between API and frontend.
Real-time subscriptions are a core requirement
GraphQL subscriptions provide a first-class interface for real-time data. The subscription type is part of the schema, tooling handles it uniformly, and clients express their real-time data needs with the same query syntax.
The N+1 Problem (Important Caveat for GraphQL)
GraphQL's relationship model creates a performance risk you must address.
Consider this query:
query {
posts { # 1 query → returns 50 posts
title
author { # 50 queries → 1 per post to fetch author
name
}
}
}
Without optimization, this is 1 + 50 = 51 database queries.
Solution: DataLoader (batching) — bundled above.
Solution: Query analysis — use tools like graphql-query-complexity to reject queries that would generate too many requests.
Solution: Join Monster or Prisma — ORMs that translate GraphQL queries to efficient SQL joins.
This doesn't exist in REST because the API designer controls what data each endpoint returns. In GraphQL, the client controls it—which requires server-side safeguards.
Hybrid Approaches
Many production systems use both.
REST for internal services (machine-to-machine, simple CRUD, file handling) + GraphQL for client-facing APIs (mobile apps, web dashboards, third-party developer portals).
GraphQL with REST backends — a GraphQL server that wraps existing REST APIs:
const resolvers = {
Query: {
user: async (_, { id }) => {
const response = await fetch(`https://legacy-api.internal/users/${id}`);
return response.json();
}
}
};
This gives clients a unified GraphQL interface while existing services stay as REST.
Modern Alternatives: tRPC
If you're building a TypeScript monorepo (Next.js app + Express/Fastify backend in one repo), tRPC is worth considering. It gives you end-to-end type safety without a schema language:
// Server
const router = t.router({
getUser: t.procedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
return db.users.findUnique({ where: { id: input.id } });
}),
});
// Client — fully typed, no code generation
const user = await trpc.getUser.query({ id: '42' });
// ^ TypeScript knows the return type automatically
tRPC works best within a single TypeScript codebase. It's not suitable for public APIs or polyglot environments.
Decision Framework
Is this a public API (consumed by external developers)?
Yes → REST
Does caching at the CDN/proxy layer matter?
Yes → REST
Is your team unfamiliar with GraphQL?
Yes → REST (for now)
Do multiple different clients (mobile, web, partner integrations) need different data shapes?
Yes → GraphQL
Is this an internal API consumed exclusively by your own frontend?
Yes → Consider GraphQL or tRPC
Do you need real-time subscriptions as a core feature?
Yes → GraphQL
Is your data model simple and resource-based?
Yes → REST
Quick Comparison Table
| Factor | REST | GraphQL |
|---|---|---|
| Learning curve | Low | Medium-High |
| HTTP caching | Native | Complex |
| Over-fetching | Common | Eliminated |
| Multiple requests | Common | Eliminated |
| Type system | Optional (OpenAPI) | Built-in |
| Tooling maturity | Excellent | Good |
| File uploads | Easy | Complex |
| Real-time | Via webhooks/SSE | First-class (subscriptions) |
| Versioning | URL/header | Schema evolution |
| Public APIs | Excellent | Possible but uncommon |
| N+1 problem | N/A | Requires DataLoader |
| Introspection | Via OpenAPI | Built-in |
Key Takeaways
- REST is not legacy. It's still the right choice for public APIs, simple CRUD, and anything where HTTP caching matters.
- GraphQL solves real problems around over-fetching, multiple round trips, and type-safe client generation—but it introduces complexity around caching, the N+1 problem, and security.
- The BFF pattern (GraphQL layer for clients, REST between services) is common in large organizations for good reason.
- tRPC is a strong alternative for TypeScript monorepos.
- The right answer depends on your clients, team, and traffic patterns—not on which technology is newer or more popular.
In 2026, neither REST nor GraphQL is going away. Knowing when to use each one is the skill worth developing.
Level Up Your Dev Workflow
Found this useful? Explore DevPlaybook — cheat sheets, tool comparisons, and hands-on guides for modern developers.
🛒 Get the DevToolkit Starter Kit on Gumroad — 40+ browser-based dev tools, source code + deployment guide included.
Top comments (0)