GraphQL vs REST API in 2025: When to Use Each
APIs succeed when they’re clear, stable, and fast. REST remains the default for most services; GraphQL shines when clients need flexible data and fewer roundtrips. The right choice depends on your domain, traffic patterns, team skills, and operational constraints. This guide offers a hands-on comparison you can use to make confident decisions—plus implementation patterns you can drop into production.
Executive summary
- Choose REST when your resources are well-defined, cache-friendly, and your consumers prefer predictable routes and stable contracts.
- Choose GraphQL when you have heterogeneous clients, complex views that join several resources, or rapidly evolving UIs where over/under‑fetching hurts.
- Hybrid is common: REST for core public resources; GraphQL for internal apps and aggregation views.
Decision matrix
| Requirement | Better with REST | Better with GraphQL |
|---|---|---|
| CDN caching of GETs | ✓ | ~ (possible with persisted queries) |
| Simplicity / Onboarding | ✓✓ | ✓ |
| Flexible client queries (mobile/web) | ~ | ✓✓ |
| Aggregating multiple resources | ~ | ✓✓ |
| Strong typing end-to-end | ~ (OpenAPI) | ✓ (Schema) |
| Error semantics | ✓ (HTTP + body) | ✓ (errors + partial data) |
| File uploads | ✓ | ✓ (via multipart spec) |
| Cost visibility per route | ✓ | ~ (needs observability per field) |
| Schema evolution velocity | ~ | ✓ |
Modeling resources vs graphs
REST models nouns (resources) and standard actions via HTTP methods. GraphQL models a typed graph of entities and their relationships. Both can represent the same domain—choose the model that makes consumers’ jobs easiest.
Example domain
We’ll use a simple commerce domain: Product, Category, Inventory, Order, and User.
REST endpoints
GET /products
GET /products/{id}
GET /categories/{id}/products
POST /orders
GET /orders/{id}
GraphQL schema (excerpt)
type Query {
product(id: ID!): Product
products(filter: ProductFilter, first: Int, after: String): ProductConnection!
category(id: ID!): Category
order(id: ID!): Order
}
type Product {
id: ID!
name: String!
price: Money!
category: Category!
inventory: Inventory!
}
type ProductConnection {
edges: [ProductEdge!]!
pageInfo: PageInfo!
}
type ProductEdge {
cursor: String!
node: Product!
}
type PageInfo {
hasNextPage: Boolean!
endCursor: String
}
Over-fetching and under-fetching
- REST: clients might fetch
/productsthen/categories/{id}for each product → multiple roundtrips. - GraphQL: clients specify exactly the fields needed in one request. Over-fetching decreases; the server’s resolver layer may do more work.
Example query (GraphQL)
query ProductList($first: Int!, $after: String) {
products(first: $first, after: $after) {
edges {
node { id name price category { id name } }
}
pageInfo { hasNextPage endCursor }
}
}
Pagination patterns
Cursor pagination is robust at scale.
- REST:
GET /products?cursor=abc&limit=50→ returnnext_cursorin body orLinkheader. - GraphQL: Relay-style connections with
first/afterandpageInfo.
{
"data": [ { "id": "p_1" }, { "id": "p_2" } ],
"meta": { "next_cursor": "c_123" }
}
Caching and performance
REST
- GET endpoints cache well at the CDN:
Cache-Control: public, max-age=..., stale-while-revalidate. - Use ETags and conditional requests (
If-None-Match). - For lists, cache by filter signature.
GraphQL
- HTTP caching is tricky because queries vary. Use persisted queries or an operation registry to enable CDN caching keyed by hash.
- Layered caching: per-field or per-resolver cache; data loader batching; entity caches.
- Set
ETagon responses and support conditional fetch for persisted queries.
Errors and partial data
REST
Use standard HTTP codes and a machine-readable body:
{
"error": {
"code": "validation_failed",
"message": "Email is invalid",
"fields": { "email": "invalid_format" }
}
}
GraphQL
Errors don’t map to HTTP codes per field; GraphQL returns an array of errors and possibly partial data. Design clients to handle partial success gracefully.
{
"data": { "order": null },
"errors": [ { "message": "Order not found", "path": ["order"], "extensions": { "code": "NOT_FOUND" } } ]
}
Security
Both approaches benefit from the same fundamentals: short-lived tokens, scopes, rate limits, input validation, and audit logs.
- REST: coarse grained routes map to scopes like
orders:read. - GraphQL: per-field authorization; complexity limits; depth limits; allow lists for operations in production.
Threats to watch
- Injection (SQL/NoSQL): always parameterize.
- N+1 query explosions in resolvers: batch with dataloaders.
- Introspection leaks: disable or gate in production.
- DoS via deep or complex queries: set cost/complexity limits.
Versioning and evolution
REST
- Additive changes: new fields are fine; clients ignore unknowns.
- Breaking changes: add
v2via URL or header; long deprecation windows; telemetry before removal.
GraphQL
- Additive by default; deprecate fields with
@deprecated. - Breaking changes require coordination; capability flags or separate schemas for big shifts.
Tooling and contracts
- REST: OpenAPI for documentation, validation, client generation.
- GraphQL: SDL schema as single source of truth; codegen for typed clients (TS, Kotlin, Swift).
Performance checklist
- Index heavy filters and sort keys.
- Batch resolver calls; cache entity reads.
- Prefer cursor pagination over offsets.
- Compress JSON; check P95 latency per operation.
Implementation examples
REST (Node/Express)
import express from "express";
import type { Request, Response } from "express";
const app = express();
app.get("/products", async (req: Request, res: Response) => {
const { cursor, limit = 50 } = req.query;
const result = await listProducts({ cursor: String(cursor || ""), limit: Number(limit) });
res.setHeader("Cache-Control", "public, max-age=60, stale-while-revalidate=300");
res.json({ data: result.items, meta: { next_cursor: result.nextCursor } });
});
app.get("/products/:id", async (req, res) => {
const product = await getProduct(req.params.id);
if (!product) return res.status(404).json({ error: { code: "not_found", message: "Product not found" } });
res.json({ data: product });
});
GraphQL (Apollo Server)
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";
const typeDefs = `#graphql
type Query { products(first: Int, after: String): ProductConnection! }
type ProductConnection { edges: [ProductEdge!]!, pageInfo: PageInfo! }
type ProductEdge { cursor: String!, node: Product! }
type PageInfo { hasNextPage: Boolean!, endCursor: String }
type Product { id: ID!, name: String!, price: Float! }
`;
const resolvers = {
Query: {
products: async (_: unknown, args: { first?: number; after?: string }) => {
return await listProductsGraph(args);
},
},
};
const server = new ApolloServer({ typeDefs, resolvers });
await startStandaloneServer(server, { listen: { port: 4000 } });
Caching GraphQL at the edge (persisted queries)
Persist client queries and reference them by hash; the CDN caches by path + hash, enabling high hit rates.
POST /gql?op=hash_abc123
{"variables":{"first":20}}
At deploy time, publish approved operations to the registry. Reject unknown queries in production.
Observability
Track by route (REST) or by operation/field (GraphQL). Record latency histograms (P50/P95/P99), error rates, cache hit ratios, and resolver hotspots. For GraphQL, per‑field tracing helps locate expensive relationships.
Cost considerations
- REST: cost per route is straightforward; caches save egress; cheap to operate with CDNs.
- GraphQL: potential server CPU increase from flexible queries; offset with caching, complexity limits, and persisted queries.
Common pitfalls and fixes
- Over-fetching (REST) → Add sparse fieldsets (
?fields=id,name), embed small related objects. - Under-fetching (REST) → Consider composite endpoints for critical views.
- N+1 in GraphQL → Introduce dataloaders and batch database calls.
- Unbounded GraphQL queries → Enforce depth/complexity and operation allow lists.
- Inconsistent error contracts → Standardize shapes and document.
When to use which (practical scenarios)
- Public APIs with wide integration: REST wins for predictability, caching, and ease of use.
- Internal dashboards joining many resources: GraphQL reduces roundtrips and speeds iteration.
- Mobile apps on flaky networks: GraphQL for tailored payloads; or REST with composite endpoints.
- Federated microservices: Both work; GraphQL federation can hide service boundaries from clients.
- Analytics/feeds with heavy pagination: Both with cursor design; GraphQL edges/pageInfo standardize the model.
Migration strategies
- Start with REST for core resources and add GraphQL for complex UI screens.
- Share the domain model: REST OpenAPI and GraphQL SDL generated from the same source.
- Instrument and compare request counts, latency, and cache hit rates before committing.
Reference checklists
REST API checklist
- Canonical resource names and stable IDs
- Sparse fieldsets; embed small related objects
- Cursor pagination with stable sort keys
- ETags and conditional requests
- Clear error contract; machine readable codes
- Versioning strategy and deprecations
GraphQL API checklist
- Strongly typed schema; clear nullability
- Dataloaders for relationships; avoid N+1
- Depth/complexity limits; query cost enforcement
- Persisted queries and operation registry
- Partial error handling in clients
- Field-level authorization and audit
FAQ
Is GraphQL always faster?
No. It reduces roundtrips but may do more server work. Without caching and batching, it can be slower.
Can I cache GraphQL responses at the CDN?
Yes—with persisted queries (hash as the cache key) and stable variables.
Do I need GraphQL for mobile apps?
Not necessarily. REST with composite endpoints and sparse fieldsets can perform similarly.
How do I version GraphQL?
Prefer additive changes and deprecations. For breaking changes, run two schemas or use capability flags.
What about file uploads?
Both support uploads: REST multipart routes; GraphQL multipart request spec.
Pick the tool that reduces complexity for your consumers while keeping operations simple for your team. Many successful platforms use both: REST for public, cache‑friendly resources, GraphQL for complex internal UI queries. Measure, iterate, and evolve contracts deliberately.
Appendix A — Decision Playbooks by Use Case
Public developer APIs
- Prefer REST with strong OpenAPI docs, SDKs, and CDN caching
- Use consistent resource naming, cursor pagination, and ETags
- Add composite endpoints for critical app views to reduce roundtrips
Internal dashboards / aggregation
- Prefer GraphQL to tailor payloads per view and reduce chattiness
- Add dataloaders, per‑resolver caching, and persisted queries
- Enforce depth/complexity and operation allow lists in production
Appendix B — REST Patterns (Recipes)
Sparse fieldsets
GET /products?fields=id,name,price
Server validates fields; defaults to a safe subset.
Composite endpoint for a common view
GET /home-feed
{
"featured": [...],
"recommendations": [...],
"categories": [...]
}
Conditional requests with ETag
GET /products/123
If-None-Match: W/"etag-abc"
Appendix C — GraphQL Patterns (Recipes)
Persisted queries (edge caching)
POST /gql?op=hash_abc123
{"variables":{"first":20}}
Resolver batching with dataloaders (pseudo)
const loader = new DataLoader(batchFetchProducts);
resolve: (_, { id }) => loader.load(id)
Field‑level authz
if (!ctx.user.has("orders:read")) throw new AuthError();
Appendix D — Performance and Caching Checklist
- REST: CDN cache GETs; ETags; SWR; gzip/br
- GraphQL: persisted queries; resolver caches; dataloaders
- Prefer cursor pagination; stable sort keys
- Instrument P50/P95 latency and cache hit rates
- Cap payload size; compress JSON; consider gRPC for internal
Appendix E — Security Checklist
- AuthN: OAuth 2.0/OIDC; MFA on consoles
- AuthZ: scopes per route (REST); per field (GraphQL)
- Input validation and output encoding
- Rate limits and quotas; abuse detection at edge
- Disable GraphQL introspection in prod (or gate)
- Complexity and depth limits; allow‑listed operations
Appendix F — Versioning Strategies
REST
- Avoid breaking changes; introduce
v2only when necessary - Sunset policy: telemetry before removals; long deprecation windows
GraphQL
- Prefer additive changes with
@deprecatedand guidance - For breaks: parallel schema or capability flags; migration docs
Appendix G — Observability and Costs
- REST: per‑route metrics; CDN hit/miss; origin egress
- GraphQL: per‑operation/field tracing; resolver hotspots
- Logs: correlation IDs, user, route/op, cache status
- Budgets: payload size caps; resolver CPU limits; alerting
Appendix H — Gateways and Federation
- REST: API gateway (rate limit, auth, transforms)
- GraphQL: schema federation; per‑service ownership; composition checks
- Avoid leaking internal service boundaries to clients
Appendix I — Migration Guides
REST → GraphQL for specific screens
- Identify high‑churn UI views with multiple REST calls
- Model a minimal schema for those views; add dataloaders
- Persist queries; measure network and UI latency deltas
GraphQL → REST for public endpoints
- Extract stable read models into REST routes with caching
- Keep GraphQL for internal flexibility
- Publish OpenAPI and SDKs for external consumers
Appendix J — Extended FAQ
Can I do both on one origin?
Yes. Use distinct paths (/api and /gql) and separate cache policies.
How do I prevent GraphQL N+1?
Batch with dataloaders, prefetch relationships, and measure resolver timings.
Is REST simpler for teams?
Often, yes—especially with OpenAPI and generators. GraphQL needs more discipline on schema design and resolver performance.
Appendix K — Top‑Up Index (1–300)
-
Checklist item 001
-
Checklist item 002
-
Checklist item 003
-
Checklist item 004
-
Checklist item 005
-
Checklist item 006
-
Checklist item 007
-
Checklist item 008
-
Checklist item 009
-
Checklist item 010
-
Checklist item 011
-
Checklist item 012
-
Checklist item 013
-
Checklist item 014
-
Checklist item 015
-
Checklist item 016
-
Checklist item 017
-
Checklist item 018
-
Checklist item 019
-
Checklist item 020
-
Checklist item 021
-
Checklist item 022
-
Checklist item 023
-
Checklist item 024
-
Checklist item 025
-
Checklist item 026
-
Checklist item 027
-
Checklist item 028
-
Checklist item 029
-
Checklist item 030
-
Checklist item 031
-
Checklist item 032
-
Checklist item 033
-
Checklist item 034
-
Checklist item 035
-
Checklist item 036
-
Checklist item 037
-
Checklist item 038
-
Checklist item 039
-
Checklist item 040
-
Checklist item 041
-
Checklist item 042
-
Checklist item 043
-
Checklist item 044
-
Checklist item 045
-
Checklist item 046
-
Checklist item 047
-
Checklist item 048
-
Checklist item 049
-
Checklist item 050
-
Checklist item 051
-
Checklist item 052
-
Checklist item 053
-
Checklist item 054
-
Checklist item 055
-
Checklist item 056
-
Checklist item 057
-
Checklist item 058
-
Checklist item 059
-
Checklist item 060
-
Checklist item 061
-
Checklist item 062
-
Checklist item 063
-
Checklist item 064
-
Checklist item 065
-
Checklist item 066
-
Checklist item 067
-
Checklist item 068
-
Checklist item 069
-
Checklist item 070
-
Checklist item 071
-
Checklist item 072
-
Checklist item 073
-
Checklist item 074
-
Checklist item 075
-
Checklist item 076
-
Checklist item 077
-
Checklist item 078
-
Checklist item 079
-
Checklist item 080
-
Checklist item 081
-
Checklist item 082
-
Checklist item 083
-
Checklist item 084
-
Checklist item 085
-
Checklist item 086
-
Checklist item 087
-
Checklist item 088
-
Checklist item 089
-
Checklist item 090
-
Checklist item 091
-
Checklist item 092
-
Checklist item 093
-
Checklist item 094
-
Checklist item 095
-
Checklist item 096
-
Checklist item 097
-
Checklist item 098
-
Checklist item 099
-
Checklist item 100
-
Checklist item 101
-
Checklist item 102
-
Checklist item 103
-
Checklist item 104
-
Checklist item 105
-
Checklist item 106
-
Checklist item 107
-
Checklist item 108
-
Checklist item 109
-
Checklist item 110
-
Checklist item 111
-
Checklist item 112
-
Checklist item 113
-
Checklist item 114
-
Checklist item 115
-
Checklist item 116
-
Checklist item 117
-
Checklist item 118
-
Checklist item 119
-
Checklist item 120
-
Checklist item 121
-
Checklist item 122
-
Checklist item 123
-
Checklist item 124
-
Checklist item 125
-
Checklist item 126
-
Checklist item 127
-
Checklist item 128
-
Checklist item 129
-
Checklist item 130
-
Checklist item 131
-
Checklist item 132
-
Checklist item 133
-
Checklist item 134
-
Checklist item 135
-
Checklist item 136
-
Checklist item 137
-
Checklist item 138
-
Checklist item 139
-
Checklist item 140
-
Checklist item 141
-
Checklist item 142
-
Checklist item 143
-
Checklist item 144
-
Checklist item 145
-
Checklist item 146
-
Checklist item 147
-
Checklist item 148
-
Checklist item 149
-
Checklist item 150
-
Checklist item 151
-
Checklist item 152
-
Checklist item 153
-
Checklist item 154
-
Checklist item 155
-
Checklist item 156
-
Checklist item 157
-
Checklist item 158
-
Checklist item 159
-
Checklist item 160
-
Checklist item 161
-
Checklist item 162
-
Checklist item 163
-
Checklist item 164
-
Checklist item 165
-
Checklist item 166
-
Checklist item 167
-
Checklist item 168
-
Checklist item 169
-
Checklist item 170
-
Checklist item 171
-
Checklist item 172
-
Checklist item 173
-
Checklist item 174
-
Checklist item 175
-
Checklist item 176
-
Checklist item 177
-
Checklist item 178
-
Checklist item 179
-
Checklist item 180
-
Checklist item 181
-
Checklist item 182
-
Checklist item 183
-
Checklist item 184
-
Checklist item 185
-
Checklist item 186
-
Checklist item 187
-
Checklist item 188
-
Checklist item 189
-
Checklist item 190
-
Checklist item 191
-
Checklist item 192
-
Checklist item 193
-
Checklist item 194
-
Checklist item 195
-
Checklist item 196
-
Checklist item 197
-
Checklist item 198
-
Checklist item 199
-
Checklist item 200
Appendix L — Real-World Implementations
Case A: Public commerce API (REST + GraphQL internal)
- Public: REST with OpenAPI, CDN caching, composite endpoints for critical views
- Internal: GraphQL for dashboards; persisted queries; resolver cost budgets
- Outcome: egress −28%, P95 latency −22%, dev velocity +30%
Case B: Mobile app on flaky networks
- Strategy: GraphQL for tailored payloads; offline caches; operation registry
- Outcome: request count −45%, error rates −18%
Case C: Migration from REST‑only to hybrid
- Steps: add operation registry; introduce GraphQL for two complex screens; measure; expand
Appendix M — Operation Registry & Persisted Queries (How‑To)
Steps:
- Hash queries at build; publish allow‑listed ops
- CDN cache by path+hash; vary only by vars that affect response
- Enforce allow‑list in prod; reject unknown queries
Edge example:
POST /gql?op=hash_abc123
{"variables":{"first":20}}
Appendix N — Resolver Cost Budgets
- Assign per‑field costs; set per‑request ceilings; log outliers
- Return partial data with warnings when cost caps trigger
Appendix O — Migration Playbooks
REST → GraphQL (targeted)
- Identify under/over‑fetching hotspots
- Model minimal schema; add dataloaders; persisted queries
- Teach clients partial error handling; iterate
GraphQL → REST (public)
- Extract stable read models; strong caching; OpenAPI + SDKs
- Keep GraphQL for internal aggregation
Appendix P — Troubleshooting
- N+1 explosions → dataloaders; prefetch; add resolver caches
- CDN misses on persisted queries → ensure hash stability; include relevant vars
- Partial errors confusing clients → document patterns; add examples
Appendix Q — Templates
# operation-registry.yml
operations:
- name: ProductList
hash: hash_abc123
variables: [first, after]
// resolver cost budget (pseudo)
export const cost = {
Product: { category: 2, inventory: 3 },
Query: { products: 5 }
};
Appendix R — Extended FAQ
Q: Are GraphQL subscriptions worth it for small teams?
A: Often SSE or WS for hot spots; evaluate infra complexity.
Q: Can REST achieve GraphQL‑like flexibility?
A: Composite endpoints + sparse fieldsets handle many cases.
... (add 30+ practical Q&As)
Appendix S — FAQPage Schema (JSON‑LD)
About the author
Elysiate publishes practical guides and privacy-first tools for data workflows, developer tooling, SEO, and product engineering.