REST API vs GraphQL: Which One Should You Use?
REST and GraphQL are the two dominant API architectures in modern web development. REST has been the industry standard since the early 2000s, while GraphQL, open-sourced by Facebook in 2015, has gained significant traction for its flexibility and developer experience. Choosing between them depends on your specific use case, team, and requirements.
The Core Difference
REST exposes multiple endpoints, each returning a fixed data structure. The server decides what data the client receives.
GraphQL exposes a single endpoint where clients specify exactly what data they need. The client decides what data it receives.
REST:
GET /users → id, name, email, address, phone, created_at
GET /users/1/posts → id, title, body, created_at
GET /users/1/posts/1/comments → ...
GraphQL:
POST /graphql → query { user(id: 1) { name posts { title } } }Data Fetching
REST
// Fetch a user and their posts — requires 2 separate requests
const user = await fetch('/api/users/1').then(r => r.json());
const posts = await fetch('/api/users/1/posts').then(r => r.json());GraphQL
// Fetch user + posts in a single request
const query = `
query {
user(id: 1) {
name
email
posts {
title
createdAt
}
}
}
`;
const data = await fetch('/graphql', {
method: 'POST',
body: JSON.stringify({ query })
---).then(r => r.json());The N+1 Problem
REST APIs that serve related resources often suffer from the N+1 problem. To display a list of blog posts with their authors, you need one request for the posts list, then N requests for each author. GraphQL solves this with batching (using tools like DataLoader), but it shifts the complexity from the client to the server.
When REST Wins
Caching
REST leverages HTTP caching natively through standard mechanisms:
GET /api/products
Cache-Control: public, max-age=300
Etag: "33a64df551425fcc55e4d42a148795d9f25f89d4"Browsers, CDNs (CloudFront, Cloudflare, Fastly), and reverse proxies (Varnish, Nginx) cache REST responses automatically. GraphQL requires manual caching through tools like Apollo Client’s normalized cache, which adds complexity and is rarely as efficient as HTTP-level caching.
File Uploads
REST handles file uploads simply with standard multipart/form-data. GraphQL requires custom implementations, multipart request specifications, or third-party extensions like Apollo Upload.
Simplicity
For simple CRUD applications with few clients (or a single frontend), REST is faster to build and easier to understand. The learning curve is minimal, documentation is extensive, and every developer already knows how REST works.
Server-side Performance
REST queries are predictable — each endpoint executes a known set of database queries. GraphQL allows clients to craft arbitrary nested queries that could strain your server. A deeply nested GraphQL query on an unprotected schema could trigger expensive JOIN operations or multiple database round trips. Mitigation requires query complexity analysis, depth limiting, and rate limiting.
When GraphQL Wins
Multiple Clients
If you support a web app (desktop), a mobile app (limited bandwidth), and a third-party API, GraphQL lets each client request exactly what it needs:
// Mobile app: needs minimal data for slow connections
query { user(id: 1) { name avatar } }
// Web dashboard: needs full user profile
query { user(id: 1) { name email address phone createdAt } }With REST, the mobile app would either over-fetch (receiving unnecessary data, slowing down over cellular) or require a separate mobile-only endpoint.
Rapid Frontend Iteration
New UI features can request new data without backend changes. The frontend team simply adds the fields they need to an existing query, as long as the data is available in the schema. This decouples frontend and backend development cycles, allowing teams to move in parallel.
Batching
GraphQL eliminates N+1 queries and over-fetching. A dashboard view that needs 10 REST endpoints becomes one GraphQL query. This is particularly valuable for complex views like admin dashboards, reporting pages, or product listing pages that aggregate data from multiple sources.
Robust Tooling
GraphQL offers excellent developer tooling:
- GraphiQL — interactive in-browser IDE for exploring schemas and testing queries
- Apollo Studio — schema registry, metrics, and observable graph management
- Code generation — generate TypeScript types from your GraphQL schema automatically
- Schema stitching — compose multiple GraphQL APIs into a single unified schema
Evolving APIs
GraphQL schemas are self-documenting through introspection. Deprecated fields remain in the schema but are marked with a @deprecated directive. Clients update at their own pace without breaking. There is no need for /v2/ endpoints or header-based versioning.
Comparison Table
| Factor | REST | GraphQL |
|---|---|---|
| Maturity | Very mature (20+ years) | Growing (10+ years) |
| Learning curve | Low | Medium |
| Caching | Built-in (HTTP) | Manual (Apollo, Relay) |
| File uploads | Simple (multipart) | Complex (custom impl) |
| Over-fetching | Common | None (client-defined) |
| Under-fetching | Common (N+1) | None (nested queries) |
| Versioning | URL or header based | Not needed (field deprecation) |
| Tooling | Extensive (Postman, curl) | Growing (GraphiQL, Apollo) |
| CDN compatibility | Excellent | Limited (POST only) |
| Query complexity | Predictable | Variable (needs protection) |
| Real-time | WebSockets + polling | Subscriptions (built-in) |
| Schema | Implicit (docs) | Explicit (SDL) |
| Type safety | Manual validation | Generated types + validation |
Hybrid Approach
Many production teams use both architectures, each for what it does best:
REST for: GraphQL for:
- Public APIs - Internal dashboards
- File uploads - Mobile apps
- Simple CRUD - Complex data views
- Cached content - Rapid prototyping
- Webhook callbacks - Cross-service aggregation
- Sitemaps / RSS - Real-time subscriptionsReal-World Hybrid Example
Shopify uses REST for storefront APIs (public, cached, simple product lookups) and GraphQL for admin APIs (complex dashboard queries, internal tooling). GitHub similarly uses REST for public API consumers and offers GraphQL for advanced use cases.
Decision Guide
Choose REST when:
- You’re building a simple CRUD API with predictable data needs
- You need excellent caching for public APIs served through a CDN
- Your API consumers are external third-party developers
- You handle large file uploads
- Your team is small and needs to ship fast without complex tooling
- You want maximum compatibility with existing infrastructure
Choose GraphQL when:
- You have multiple clients with different data needs (web, mobile, third-party)
- You’re building complex dashboards or views that aggregate from multiple sources
- Your frontend team iterates faster than your backend
- You want a self-documenting, strongly typed API contract
- You need real-time subscriptions alongside your query API
- You have the engineering bandwidth to invest in schema design and performance protection
Performance Considerations
Query Complexity Analysis
GraphQL servers should limit query complexity to prevent abusive queries. Calculate complexity based on field depth and data fetching costs:
// Apollo Server complexity analysis
const server = new ApolloServer({
schema,
validationRules: [
complexityEstimator({
defaultComplexity: 1,
estimators: [
fieldConfigEstimator(),
simpleEstimator({ defaultComplexity: 1 })
],
maximumComplexity: 1000,
})
]
---);Set limits on query depth (e.g., max 7 levels) and query breadth (e.g., max 50 fields per query). These guardrails prevent accidental or malicious performance degradation.
Batched Resolvers with DataLoader
DataLoader deduplicates and batches database requests within a single GraphQL request:
const DataLoader = require('dataloader');
const userLoader = new DataLoader(async (ids) => {
const users = await db.select('*').from('users').whereIn('id', ids);
return ids.map(id => users.find(u => u.id === id));
---);
// In resolver:
User: {
posts: (parent) => postLoader.load(parent.id)
---Without DataLoader, fetching 100 posts with authors would generate 101 database queries. With DataLoader, it’s 2 queries — one for the posts, one for the authors, batched into a single WHERE IN clause.
The right answer for most teams is both. Start with REST for your public-facing, cache-friendly endpoints. Add GraphQL for your internal or mobile-facing needs where flexibility matters more than caching.
FAQ
Can I use GraphQL with a REST API?
Yes. Many teams expose a GraphQL gateway that sits in front of existing REST APIs. The GraphQL resolvers call internal REST endpoints and transform the responses. This allows incremental adoption without rewriting existing services.
Does GraphQL replace REST entirely?
Rarely. Most production systems use both. REST excels at caching, file uploads, and simple CRUD — things GraphQL handles poorly. GraphQL excels at complex queries, real-time subscriptions, and flexible data fetching.
How do I authenticate GraphQL requests?
Authentication happens at the HTTP layer, not the GraphQL layer. Use the Authorization header with a Bearer token, just like REST. The GraphQL context extracts the authenticated user and passes it to resolvers.
Is GraphQL slower than REST?
GraphQL can be slower for simple requests due to query parsing and validation overhead. For complex requests with multiple related resources, GraphQL is typically faster because it eliminates multiple round trips.
What are GraphQL subscriptions?
Subscriptions are GraphQL’s real-time feature. Clients subscribe to events (e.g., “new message”) and receive push updates via WebSocket. This is built into the GraphQL specification, unlike REST which requires separate WebSocket endpoints or polling.
Related: Learn Chrome DevTools for debugging API calls.
For a comprehensive overview, read our article on Advanced Git Commands.