Quick answer
REST uses fixed endpoints that return predetermined data shapes; GraphQL uses a single endpoint where the client specifies exactly which fields it needs. Use REST for public APIs, simple services, and when HTTP caching matters. Use GraphQL when multiple clients (web, mobile, TV) need different shapes of the same data, or when round-trips between requests are a measured problem.
The same data request in REST and GraphQL
Both styles use JSON. The difference is who controls the shape of the response.
REST: fixed shape, multiple endpoints
# Get a user — returns all fields the server decided to include
GET /api/users/42
→ { "id": 42, "name": "Alice", "email": "...", "address": {...}, "billing": {...}, "preferences": {...} }
# Get that user's posts — separate request
GET /api/users/42/posts
→ [ { "id": 1, "title": "...", "body": "...", "createdAt": "..." }, ... ]
# Two HTTP requests to assemble one screen
GraphQL: one endpoint, client-defined shape
# Single POST to /graphql — client asks for exactly what it needs
POST /graphql
{
"query": "{ user(id: 42) { name posts { title } } }"
}
→ {
"data": {
"user": {
"name": "Alice",
"posts": [
{ "title": "My first post" },
{ "title": "Another post" }
]
}
}
}
# One HTTP request, only the fields requested
The mobile app only needed name and post title. REST returned 30+ fields and required 2 requests. GraphQL returned exactly 3 fields in 1 request.
Quick comparison
| Feature | REST | GraphQL |
|---|---|---|
| Endpoints | Multiple (one per resource) | Single /graphql |
| Response shape | Fixed — server decides | Flexible — client decides |
| Over-fetching | Common | Eliminated |
| Under-fetching | Common (N+1 requests) | Eliminated (one query) |
| HTTP caching | Native (GET is cacheable) | Requires custom approach |
| Versioning | URL versioning (/v1/) | Evolve schema without versions |
| Type system | None built-in (use OpenAPI) | Built-in SDL schema |
| API documentation | OpenAPI / Swagger (external spec) | Built-in introspection (GraphiQL / Apollo Studio) |
| Real-time | SSE / WebSockets (manual) | Subscriptions (built-in pattern) |
| File upload | Simple multipart/form-data | Requires extensions (messy) |
| Learning curve | Low — universally understood | Moderate — SDL, resolvers, DataLoader |
| Best for | Public APIs, simple CRUD, CDN caching | Multiple clients, complex data graphs |
Over-fetching and under-fetching explained
Over-fetching
A REST endpoint returns everything it has, regardless of what the client needs. A user profile endpoint might return name, email, address, billing, preferences, avatar URL, last login timestamp, and account tier — but a mobile header component only needs the name and avatar. The other 25 fields are wasted bandwidth on every request.
On a mobile connection at 2G speeds, or multiplied across 10 million API calls per day, over-fetching becomes a real cost — in bandwidth, battery, and parse time.
Under-fetching (the N+1 problem)
A single REST endpoint often does not return all the data a screen needs, so the client makes multiple sequential requests — first for a list, then for details of each item in the list:
# Client building a blog post list page with author names
GET /posts → [ { id: 1, authorId: 5 }, { id: 2, authorId: 3 }, ... ]
GET /users/5 → { name: "Alice" }
GET /users/3 → { name: "Bob" }
# ... 1 + N requests for N authors
# GraphQL collapses this to one round-trip:
{ posts { title author { name } } }
HTTP caching: REST’s biggest advantage
REST GET requests are natively cacheable by browsers, CDNs (Cloudflare, Fastly), and API gateways. A response to GET /api/products/123 can be cached for 5 minutes at the CDN edge, serving thousands of requests without hitting your origin server.
GraphQL typically uses POST for queries (because query strings can be long). POST requests are not cached by default at the CDN layer. Solutions exist — persisted queries, GET-based queries, CDN-specific rules — but they add operational complexity that REST avoids entirely.
For read-heavy public APIs (product catalogs, documentation, publicly cached data), REST’s caching story is significantly simpler.
Type system and schema
GraphQL has a built-in Schema Definition Language (SDL). Every field, type, and relationship in your API is declared in the schema and automatically introspectable. Clients can query __schema to discover everything the API offers. Tools like GraphiQL and Apollo Studio generate interactive documentation automatically from the schema.
# GraphQL SDL — the schema is the contract
type Query {
user(id: ID!): User
posts(authorId: ID): [Post!]!
}
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
body: String!
author: User!
createdAt: String!
}
REST has no built-in equivalent, but OpenAPI (Swagger) fills the same role — documenting endpoints, request shapes, and response types. OpenAPI is a separate document you maintain alongside the code; GraphQL’s schema is the code.
Versioning
REST typically versions via the URL (/v1/users, /v2/users) or a header (API-Version: 2). Old versions must be maintained while clients migrate, creating long-term maintenance debt.
GraphQL avoids explicit versioning by adding new fields and deprecating old ones within the same schema. Clients that query only the old fields continue working; new clients use the new fields. The trade-off is that the schema accumulates deprecated fields over time if you are not disciplined about removing them.
Real-time: Subscriptions vs SSE
GraphQL has Subscriptions as a first-class concept — they are defined in the schema alongside queries and mutations. The underlying transport is typically WebSockets. Clients subscribe to an event (e.g., a new message in a chat room) and receive updates pushed by the server.
# GraphQL Subscription
subscription {
messageAdded(roomId: "general") {
id
text
author { name }
}
}
REST does not define a real-time pattern. The common options are Server-Sent Events (SSE) for one-way push, or WebSockets for bidirectional. Both work well but require more manual setup than GraphQL’s built-in subscription pattern.
When to use REST
- Public APIs consumed by third-party developers — REST is universally understood and requires no SDK
- Simple CRUD services where endpoints map cleanly to resources
- APIs where HTTP caching at a CDN is critical for performance or cost
- File uploads, binary payloads, or streaming responses
- Microservices communicating with each other — REST is lightweight and tooling support is universal
- Teams new to APIs — REST has a significantly lower barrier to entry
When to use GraphQL
- Products with multiple clients (web, iOS, Android, desktop) that need different data shapes from the same backend
- Dashboards and data-heavy UIs that aggregate from multiple data sources in one screen
- Rapid frontend iteration where the mobile team should not wait for backend changes to add or remove fields
- When you want self-documenting APIs with built-in introspection (useful for internal developer portals)
- Real-time features where GraphQL Subscriptions fit naturally
GraphQL disadvantages
GraphQL’s flexibility comes with trade-offs you have to actively manage. None of these are dealbreakers, but they are the reasons many teams stay on REST for simpler services:
- The N+1 resolver problem. A resolver that fetches a list of N items and then resolves a related field per item triggers N additional queries. Fetching 100 posts plus each author can mean 101 database queries. The fix is the DataLoader pattern, which batches and deduplicates those queries within a single request — but you have to wire it up deliberately.
- Complex caching. Because queries are typically
POSTrequests, you lose the native HTTP and CDN caching that REST gets for free onGET. Recovering it requires persisted queries, GET-based queries, or normalized client caches (Apollo, Relay). - Harder observability. Every operation is a single
POST /graphql 200. Per-endpoint metrics, logging, rate limiting, and error rates that REST exposes by URL now require GraphQL-aware tooling that breaks down by operation name and resolver. - More complex authorization. Auth in REST is per-route and easy to reason about. In GraphQL a single query can touch many types, so authorization has to be enforced at the field or resolver level — more surface area to get wrong.
- Query cost and abuse. Clients control the query shape, so a single request can ask for something enormously expensive. This needs its own defenses — see below.
Query cost and security
The same flexibility that eliminates over-fetching also lets a client send a deeply nested, cyclic query that can exhaust your server — a denial-of-service vector that a fixed REST endpoint simply does not have:
# A single malicious query — cheap to send, expensive to resolve
{
users {
posts {
comments {
author {
posts {
comments {
author { name }
}
}
}
}
}
}
}
Because users → posts → comments → author can cycle indefinitely, one short request can fan out into millions of database reads. Production GraphQL APIs defend against this with several layers:
- Depth limiting — reject any query nested beyond a fixed depth (e.g. 7 levels) before it executes.
- Query complexity / cost analysis — assign each field a cost, sum the query’s total, and reject queries over a budget. Libraries like
graphql-cost-analysisandgraphql-query-complexityautomate this. - Persisted queries (allow-list) — pre-register the exact queries your clients are allowed to send; reject anything not on the list. This eliminates arbitrary queries entirely in production.
- Timeouts — cap how long any single resolver or request may run, so a slow query fails fast instead of holding resources.
- Pagination with limits — require and cap
first/limitarguments on list fields so a query cannot request unbounded collections.
REST sidesteps all of this: each endpoint returns a known, bounded shape, so there is no client-controlled query to attack with. This is one of the clearest cases where REST’s rigidity is a security advantage.
Using both: the BFF pattern
Many mature engineering teams run REST and GraphQL side by side. The Backend For Frontend (BFF) pattern uses GraphQL as an aggregation layer in front of multiple REST or gRPC microservices. Each client (web, mobile) has its own BFF that queries the microservices and shapes the response for that client specifically.
# Architecture: GraphQL BFF in front of REST microservices
[Mobile App] ──── GraphQL ────> [Mobile BFF] ──> /users (REST)
[Web App] ──── GraphQL ────> [Web BFF] ──> /posts (REST)
──> /search (REST)
──> /media (REST)
The microservices stay simple REST services. The GraphQL BFF handles data aggregation and client-specific shaping. This gives you the simplicity of REST internally and the flexibility of GraphQL externally.
Frequently Asked Questions
What is the main difference between REST and GraphQL?
REST exposes fixed endpoints — each URL returns a predetermined shape of data. GraphQL exposes a single endpoint where the client sends a query describing exactly which fields it wants. REST can over-fetch (returning more data than needed) or under-fetch (requiring multiple requests); GraphQL solves both by letting the client specify the exact shape of the response in every request.
Is GraphQL faster than REST?
GraphQL can reduce round-trips by combining multiple data needs into one request, which is faster on slow connections. However, a simple REST endpoint serving one well-known shape is often faster because it can be cached at the CDN or HTTP layer without additional configuration. GraphQL POST requests are not cacheable by default. Performance depends more on implementation quality than on which style you choose.
Should I use REST or GraphQL for a public API?
REST is generally the better choice for public APIs. It has a lower learning curve for consumers, is universally understood with no SDK required, and works well with HTTP caching and standard API gateways. GraphQL is powerful for internal APIs and products with multiple clients (web, mobile, TV) that need different data shapes from the same backend.
Does GraphQL replace REST?
No. GraphQL solves specific problems — over-fetching, under-fetching, rapid frontend iteration — but REST is simpler to cache, monitor, and scale for most use cases. Many companies run both side by side: REST for external and public-facing endpoints, GraphQL as a BFF layer for internal data aggregation across multiple clients.
What does over-fetching mean in REST?
Over-fetching means a REST endpoint returns more fields than the client needs. A mobile app fetching a user profile may only need the name and avatar, but the endpoint returns 30 fields including address, billing info, and preferences. That unused data wastes bandwidth on every request. GraphQL solves this by letting the client request only the specific fields it needs.
What is the N+1 problem in GraphQL?
The N+1 problem occurs when a GraphQL resolver fetches a list of N items and then makes one additional database query per item to resolve a related field. Fetching 100 posts and then the author for each results in 101 database queries. The standard solution is the DataLoader pattern, which batches those N queries into a single query and caches within the same request.
What are the main disadvantages of GraphQL?
GraphQL’s main trade-offs are: no native HTTP/CDN caching (POST requests are not cacheable by default), the N+1 resolver problem which needs DataLoader batching, harder observability because every operation is a single POST /graphql 200 rather than distinct endpoints, field-level authorization that is more complex than per-route checks, and exposure to expensive nested queries that require depth limiting and complexity analysis to prevent abuse. REST avoids most of these by design.
How do you secure a GraphQL API?
Secure a GraphQL API with several layers: depth limiting to reject deeply nested queries, query complexity / cost analysis to score and cap expensive queries before execution, persisted queries (an allow-list of approved queries) so clients cannot send arbitrary ones, server-side timeouts, and mandatory pagination limits on list fields. These defend against denial-of-service from a single malicious query, which a fixed REST endpoint is not vulnerable to.
Working with REST or GraphQL responses?
Format and validate any JSON response in your browser — nothing is sent to a server.