Overview
REST and GraphQL are two different answers to "how should a client ask a server for data." REST models an API as a set of resources, each with a URL and a fixed response shape per HTTP verb; GraphQL models an API as a single typed schema that clients query with exactly the fields they need. Neither is universally better, REST's simplicity and native HTTP caching make it the right default for most APIs, while GraphQL's flexible field selection pays off specifically when many different clients need meaningfully different shapes of the same underlying data.
Quick Reference
| REST | GraphQL | |
|---|---|---|
| Shape | Fixed per endpoint | Client-specified per query |
| Over/under-fetching | Common | Solved by design |
| Caching | Native (HTTP, CDN, browser) | Requires client-side normalized cache |
| Versioning | New endpoint or /v2/ prefix | Schema evolves additively, fewer breaking versions |
| Tooling maturity | Very mature, ubiquitous | Mature, more setup (schema, resolvers) |
| Best fit | Simple, resource-shaped APIs | Many client shapes, complex data graphs |
Syntax
# REST - fixed response shape per resource
GET /api/users/42
GET /api/users/42/orders?status=pending# GraphQL - client specifies exactly the fields it needs
query {
user(id: 42) {
name
orders(status: PENDING) {
id
total
}
}
}Examples
// REST client: shape is whatever the endpoint returns, take it or leave it
const res = await fetch('/api/users/42')
const user = await res.json() // full user object, even if only `name` is needed// GraphQL client: request only the fields actually used
const res = await fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `query { user(id: 42) { name } }`,
}),
})
const { data } = await res.json()Visual Diagram
Common Mistakes
- Adopting GraphQL for a small, simple API where REST would have been simpler and required no new tooling or caching strategy.
- Not solving the N+1 query problem in GraphQL resolvers, a naive resolver for
orders { user { name } }across a list can issue one database query per item unless batched (typically via a DataLoader pattern). - Assuming GraphQL is automatically faster because it avoids over-fetching, a poorly designed schema with expensive nested resolvers can be slower than a well-designed REST endpoint.
- Ignoring REST's native cacheability and rebuilding equivalent caching logic from scratch on top of GraphQL without a real need for its flexibility.
Performance
- REST responses can be cached at the CDN/browser layer by URL for free; GraphQL generally cannot, unless using persisted queries (a hashed query id used as a stable cache key) or a normalized client-side cache.
- GraphQL's flexible querying can let a client accidentally request a deeply nested, expensive query, production GraphQL servers typically enforce query depth/complexity limits to prevent this.
- Batching (DataLoader or equivalent) is close to mandatory for GraphQL resolvers that fan out to a database or another service, to avoid N+1 query patterns.
Best Practices
- Default to REST for straightforward, resource-shaped APIs with a small number of client types.
- Reach for GraphQL when multiple, meaningfully different clients need different shapes of the same data graph.
- If using GraphQL, batch resolver data-fetching (DataLoader pattern) and enforce query complexity limits from day one.
- If using REST, design consistent, predictable resource URLs and use query parameters for filtering rather than proliferating near-duplicate endpoints.