API design comparison
REST vs GraphQL
Two different ways to design an HTTP API. Choosing between them is one of the most discussed architectural decisions of the last decade — and one of the most common topics in code reviews and system-design interviews.
TL;DR
- REST exposes resources at stable URLs (one URL per resource type). Clients call multiple endpoints to assemble what they need. Great for simple, cacheable, public APIs.
- GraphQL exposes a single endpoint with a typed schema. Clients write a query describing exactly the data they want. Great for product APIs powering many frontends with different data needs.
- Neither is universally better. The choice depends on your client diversity, caching needs, schema evolution discipline, and team experience.
Side-by-side comparison
| Aspect | REST | GraphQL |
|---|---|---|
| Shape | Multiple URL endpoints per resource | Single endpoint, query-driven |
| Data fetching | Fixed response per endpoint; over- or under-fetching common | Client picks exact fields → no over-fetching |
| Verbs / methods | HTTP verbs: GET, POST, PUT, PATCH, DELETE | Query, Mutation, Subscription (over POST or WebSocket) |
| Type system | Optional (OpenAPI / JSON Schema) | Built-in, mandatory schema (SDL) |
| Versioning | /v1/, /v2/ in URL or header | Schema evolution + @deprecated |
| Caching | HTTP caching out of the box (CDN-friendly) | Normalised client caches; harder on the wire |
| Error handling | HTTP status codes (4xx / 5xx) | Always 200 OK; errors in "errors" array |
| N+1 problem | Client orchestrates calls (front-end sees it) | Resolvers expose it on the server (DataLoader pattern) |
| Tooling | Mature: Swagger, Postman, curl | Strong: GraphiQL, Apollo Studio, codegen |
| Learning curve | Low — HTTP knowledge is enough | Higher — schema design, resolvers, federation |
| Best for | Public APIs, partner integrations, file uploads | Product APIs, mobile + web sharing one backend |
Code side-by-side
Fetching a user with their last 5 orders:
REST (often 2 calls)
GET /api/users/42
→ { "id": 42, "name": "Alice", "email": "..." }
GET /api/users/42/orders?limit=5
→ [
{ "id": 901, "total": 42.00, "createdAt": "..." },
{ "id": 887, "total": 19.50, "createdAt": "..." },
...
] GraphQL (one call)
POST /graphql
{
user(id: 42) {
name
email
orders(limit: 5) {
id
total
createdAt
}
}
} When to use REST
- Public API consumed by third parties. REST is the lingua franca — every language has an HTTP client; every developer knows GET/POST.
- You need HTTP caching. Static-ish resources cached at the CDN edge are dramatically cheaper than running every query through your servers.
- File uploads / downloads. GraphQL handles these awkwardly; multipart uploads via REST are simpler.
- Webhooks and event delivery. Outbound events to partner systems are almost always REST POST callbacks.
- Small team, no GraphQL experience. REST has lower ramp-up cost and fewer footguns.
When to use GraphQL
- Multiple clients with different data needs. Mobile, web, and partner apps can each ask for what they need without separate endpoints.
- Aggregating multiple backend resources. A GraphQL layer becomes a thin orchestration on top of microservices.
- Strong schema discipline desired. The mandatory schema becomes the contract between teams; codegen produces typed clients automatically.
- Rapid product iteration. Adding fields without releasing new endpoints reduces deploy coupling.
- Federated architecture. Multiple teams own subgraphs that compose into one supergraph (Apollo Federation, Hot Chocolate, etc.).
English phrases engineers use
REST conversations
- "We expose an endpoint at
/api/users/:id." - "This returns a 404 if the resource is missing."
- "The response is idempotent — same input, same output."
- "Let's version the API behind
/v2/." - "The endpoint is over-fetching — the mobile app only needs three of the twelve fields."
GraphQL conversations
- "We have a schema-first approach — the SDL is the source of truth."
- "The resolver fetches related orders from the orders service."
- "We're hitting N+1 on the comments resolver — let's add a DataLoader."
- "The field is deprecated with
@deprecated(reason: ...)— clients have 60 days to migrate." - "This is a breaking change on the schema — we'll need a coordinated rollout."
Quick decision tree
- Public API consumed by external partners → REST
- Need aggressive HTTP/CDN caching → REST
- One backend, many frontends with different data needs → GraphQL
- Mobile-first product where round trips hurt → GraphQL
- Many microservices, want a single client API → GraphQL (Federation)
- Team has no GraphQL experience and a tight deadline → REST
- Need file uploads → REST (or hybrid)
Common Mistakes and Trade-offs
A remarkably common mistake teams make when evaluating REST versus GraphQL is prioritizing initial complexity reduction over long-term maintainability. Many developers, especially those new to API design, instinctively choose GraphQL because it looks simpler – a single endpoint, no need to define multiple resource representations. However, this often leads to poorly designed schemas that attempt to encompass every possible data requirement, resulting in massive, monolithic queries and eventual performance bottlenecks. The initial reduction in effort is quickly outweighed by the increased debugging time, complex query optimization, and difficulty in evolving the schema as business needs change. It's a classic case of solving one problem (complex client requirements) with another that's just as challenging – or more so – without any fundamental architectural improvements.
At scale, GraphQL introduces significant operational overhead related to caching. While REST APIs can leverage standard HTTP caching mechanisms effectively, GraphQL's ability to return deeply nested data structures necessitates a much more sophisticated and granular caching strategy. Each field query is essentially a separate request, and the impact of cache invalidation becomes exponentially complex as the number of resolvers (the functions that fetch the data for each field) grows. Without careful consideration of caching layers – often involving server-side proxies or dedicated GraphQL caching solutions – performance can degrade rapidly under heavy load, turning what was initially a speed advantage into a significant operational burden. This isn't about simply adding a CDN; it's about fundamentally rethinking how data is served and cached.
The persistent misconception that GraphQL is 'always better' for new applications is dangerously misleading. While GraphQL excels at scenarios with diverse client requirements – particularly mobile apps or those needing to fetch data from multiple, disparate sources – REST remains the superior choice when simplicity and straightforwardness are paramount. For basic CRUD operations on a relatively static dataset, a well-designed REST API built with clear resource definitions is often faster to develop, easier to understand, and less prone to errors. Over-engineering a simple application with GraphQL introduces unnecessary complexity that adds no value and increases the risk of introducing bugs. It's about choosing the right tool for the job, not forcing a single technology onto every situation.
Migrating from a REST API to GraphQL – or, more commonly, combining them strategically – requires careful planning beyond simply swapping out endpoints. Often, teams attempt a wholesale conversion, leading to significant disruption and rework. A more pragmatic approach involves identifying distinct 'domains' within the application where GraphQL's strengths align with the existing REST infrastructure. For example, public-facing APIs might remain RESTful while internal microservices leverage GraphQL for optimized data retrieval. Furthermore, consider using GraphQL as a layer on top of existing REST services; this allows you to gradually adopt GraphQL without completely rewriting your core system and provides a fallback mechanism if issues arise. The key is not to treat them as mutually exclusive technologies but rather as complementary tools within a broader architectural strategy.
Frequently asked questions
What is the main difference between REST and GraphQL in plain English?
REST organises an API around resources (URLs like /users/42), and clients call separate endpoints to fetch related data. GraphQL exposes a single endpoint and lets clients describe exactly the shape of the data they want in one query — including nested resources.
When should I use REST instead of GraphQL?
Use REST when your API is simple and resource-shaped (CRUD on a few entities), when HTTP caching matters (REST plays well with CDNs), when consumers are external partners who expect a conventional REST API, or when your team has no in-house GraphQL experience.
When is GraphQL the better choice?
Pick GraphQL when clients (mobile, multiple frontends) need to compose data from many backend resources and want to avoid over-fetching; when you have a typed schema you want to share across teams; or when you need strong client-side tooling (auto-generated types, IDE autocomplete).
Is GraphQL faster than REST?
Not inherently. GraphQL can reduce the number of round trips (one query instead of N REST calls), which often feels faster on mobile networks. But a single GraphQL query can be more expensive on the server, especially with deeply nested resolvers. The right comparison is "total latency for the actual workload", not raw protocol overhead.
Do REST and GraphQL handle versioning differently?
REST typically versions through the URL (/v1/users, /v2/users) or an Accept header. GraphQL discourages versioning — instead, you evolve the schema by adding new fields and deprecating old ones with the @deprecated directive. Clients only break if they actively request a removed field.
Which is easier to cache?
REST is significantly easier to cache because each URL maps to a stable response, so HTTP caching, CDNs, and ETag/If-None-Match all work out of the box. GraphQL traditionally uses POST for queries, which most CDNs do not cache by default — you typically rely on client-side normalised caches (Apollo, urql, Relay) instead.
How do error responses compare?
REST uses HTTP status codes (4xx for client errors, 5xx for server errors) and a body schema. GraphQL always returns 200 OK and reports errors in a top-level "errors" array — the response can be partially successful (some fields resolved, others failed). This trips up engineers used to REST conventions.