The Fundamental Difference
REST models your API as a set of resources, each with its own URL. Clients navigate resources by following URLs, and the server decides what data is returned per endpoint.
GraphQL exposes a single endpoint (/graphql). Clients send a query describing *exactly* the data they need, and the server returns exactly that — no more, no less.
# GraphQL — client specifies shape
query {
user(id: "42") {
name
email
posts(first: 3) {
title
}
}
}
The equivalent in REST might require two requests: GET /users/42 + GET /users/42/posts?limit=3.
Over-fetching and Under-fetching
Over-fetching (REST): a GET /users/42 endpoint returns 30 fields, but the mobile client only needs name and avatar_url. The extra data wastes bandwidth.
Under-fetching (REST): a single endpoint does not return enough data, forcing multiple round-trips. A page showing a user's profile, their latest 5 posts, and follower count needs 3+ separate REST requests.
GraphQL eliminates both problems by letting the client declare the exact shape it needs. This is especially valuable for mobile clients on metered connections.
N+1 Query Problem
GraphQL introduces its own pitfall: the N+1 query problem.
query {
posts {
title
author { name } # N separate DB queries for N posts
}
}
Without optimization, fetching 100 posts triggers 100 additional queries to fetch each author. The solution is the DataLoader pattern: batch and cache all author IDs, then fetch them in a single query.
REST avoids N+1 naturally because the server controls the join logic — but only by forcing clients to accept whatever the server bundles together.
Schema vs Endpoint Design
GraphQL has a strongly-typed schema that serves as a contract between client and server. Tools can auto-generate TypeScript types, validate queries at build time, and power IDE autocompletion.
REST relies on OpenAPI (or similar) for schema documentation, which is external to the protocol and often lags behind the implementation.
Error Handling Comparison
| REST | GraphQL | |
|---|---|---|
| Success | `200 OK` | `200 OK` (always) |
| Not found | `404 Not Found` | `200 OK` + `errors` array |
| Partial success | Rare | Common — data + errors coexist |
| Error format | Varies | Standardized `errors` array |
GraphQL's always-200 approach surprises many developers and complicates HTTP-level monitoring (your 200 rate means nothing in GraphQL). Always check the errors field in GraphQL responses.
Performance Trade-offs
- Caching: REST is HTTP cache-friendly — GET requests cache at the CDN and browser level. GraphQL POST requests do not cache by default (though persisted queries and GET-based queries help).
- Query complexity: malicious or poorly-written GraphQL queries can trigger deeply nested resolvers. Rate-limit by query complexity, not just request count.
- Bundle size: GraphQL client libraries (Apollo, Relay) add significant JS weight compared to a plain
fetch()call.
Migration Path
If you have an existing REST API and want GraphQL benefits, consider a GraphQL gateway that wraps your REST services. This lets you expose a GraphQL schema to frontend clients without rewriting backend logic.
Choose REST when: you have a public API consumed by diverse clients, you need HTTP caching, or simplicity and broad tooling support matter.
Choose GraphQL when: you have multiple clients (web, iOS, Android) with very different data needs, you want to reduce mobile bandwidth, or you value strong schema typing for large teams.