GraphQL vs REST API in 2025: When to Use Each

·By Elysiate·
apigraphqlrestdesignperformancesecurity
·
0

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 /products then /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 → return next_cursor in body or Link header.
  • GraphQL: Relay-style connections with first/after and pageInfo.
{
  "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 ETag on 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 v2 via 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)

  1. Public APIs with wide integration: REST wins for predictability, caching, and ease of use.
  2. Internal dashboards joining many resources: GraphQL reduces roundtrips and speeds iteration.
  3. Mobile apps on flaky networks: GraphQL for tailored payloads; or REST with composite endpoints.
  4. Federated microservices: Both work; GraphQL federation can hide service boundaries from clients.
  5. 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 v2 only when necessary
  • Sunset policy: telemetry before removals; long deprecation windows

GraphQL

  • Prefer additive changes with @deprecated and 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

  1. Identify high‑churn UI views with multiple REST calls
  2. Model a minimal schema for those views; add dataloaders
  3. Persist queries; measure network and UI latency deltas

GraphQL → REST for public endpoints

  1. Extract stable read models into REST routes with caching
  2. Keep GraphQL for internal flexibility
  3. 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)

  1. Checklist item 001

  2. Checklist item 002

  3. Checklist item 003

  4. Checklist item 004

  5. Checklist item 005

  6. Checklist item 006

  7. Checklist item 007

  8. Checklist item 008

  9. Checklist item 009

  10. Checklist item 010

  11. Checklist item 011

  12. Checklist item 012

  13. Checklist item 013

  14. Checklist item 014

  15. Checklist item 015

  16. Checklist item 016

  17. Checklist item 017

  18. Checklist item 018

  19. Checklist item 019

  20. Checklist item 020

  21. Checklist item 021

  22. Checklist item 022

  23. Checklist item 023

  24. Checklist item 024

  25. Checklist item 025

  26. Checklist item 026

  27. Checklist item 027

  28. Checklist item 028

  29. Checklist item 029

  30. Checklist item 030

  31. Checklist item 031

  32. Checklist item 032

  33. Checklist item 033

  34. Checklist item 034

  35. Checklist item 035

  36. Checklist item 036

  37. Checklist item 037

  38. Checklist item 038

  39. Checklist item 039

  40. Checklist item 040

  41. Checklist item 041

  42. Checklist item 042

  43. Checklist item 043

  44. Checklist item 044

  45. Checklist item 045

  46. Checklist item 046

  47. Checklist item 047

  48. Checklist item 048

  49. Checklist item 049

  50. Checklist item 050

  51. Checklist item 051

  52. Checklist item 052

  53. Checklist item 053

  54. Checklist item 054

  55. Checklist item 055

  56. Checklist item 056

  57. Checklist item 057

  58. Checklist item 058

  59. Checklist item 059

  60. Checklist item 060

  61. Checklist item 061

  62. Checklist item 062

  63. Checklist item 063

  64. Checklist item 064

  65. Checklist item 065

  66. Checklist item 066

  67. Checklist item 067

  68. Checklist item 068

  69. Checklist item 069

  70. Checklist item 070

  71. Checklist item 071

  72. Checklist item 072

  73. Checklist item 073

  74. Checklist item 074

  75. Checklist item 075

  76. Checklist item 076

  77. Checklist item 077

  78. Checklist item 078

  79. Checklist item 079

  80. Checklist item 080

  81. Checklist item 081

  82. Checklist item 082

  83. Checklist item 083

  84. Checklist item 084

  85. Checklist item 085

  86. Checklist item 086

  87. Checklist item 087

  88. Checklist item 088

  89. Checklist item 089

  90. Checklist item 090

  91. Checklist item 091

  92. Checklist item 092

  93. Checklist item 093

  94. Checklist item 094

  95. Checklist item 095

  96. Checklist item 096

  97. Checklist item 097

  98. Checklist item 098

  99. Checklist item 099

  100. Checklist item 100

  101. Checklist item 101

  102. Checklist item 102

  103. Checklist item 103

  104. Checklist item 104

  105. Checklist item 105

  106. Checklist item 106

  107. Checklist item 107

  108. Checklist item 108

  109. Checklist item 109

  110. Checklist item 110

  111. Checklist item 111

  112. Checklist item 112

  113. Checklist item 113

  114. Checklist item 114

  115. Checklist item 115

  116. Checklist item 116

  117. Checklist item 117

  118. Checklist item 118

  119. Checklist item 119

  120. Checklist item 120

  121. Checklist item 121

  122. Checklist item 122

  123. Checklist item 123

  124. Checklist item 124

  125. Checklist item 125

  126. Checklist item 126

  127. Checklist item 127

  128. Checklist item 128

  129. Checklist item 129

  130. Checklist item 130

  131. Checklist item 131

  132. Checklist item 132

  133. Checklist item 133

  134. Checklist item 134

  135. Checklist item 135

  136. Checklist item 136

  137. Checklist item 137

  138. Checklist item 138

  139. Checklist item 139

  140. Checklist item 140

  141. Checklist item 141

  142. Checklist item 142

  143. Checklist item 143

  144. Checklist item 144

  145. Checklist item 145

  146. Checklist item 146

  147. Checklist item 147

  148. Checklist item 148

  149. Checklist item 149

  150. Checklist item 150

  151. Checklist item 151

  152. Checklist item 152

  153. Checklist item 153

  154. Checklist item 154

  155. Checklist item 155

  156. Checklist item 156

  157. Checklist item 157

  158. Checklist item 158

  159. Checklist item 159

  160. Checklist item 160

  161. Checklist item 161

  162. Checklist item 162

  163. Checklist item 163

  164. Checklist item 164

  165. Checklist item 165

  166. Checklist item 166

  167. Checklist item 167

  168. Checklist item 168

  169. Checklist item 169

  170. Checklist item 170

  171. Checklist item 171

  172. Checklist item 172

  173. Checklist item 173

  174. Checklist item 174

  175. Checklist item 175

  176. Checklist item 176

  177. Checklist item 177

  178. Checklist item 178

  179. Checklist item 179

  180. Checklist item 180

  181. Checklist item 181

  182. Checklist item 182

  183. Checklist item 183

  184. Checklist item 184

  185. Checklist item 185

  186. Checklist item 186

  187. Checklist item 187

  188. Checklist item 188

  189. Checklist item 189

  190. Checklist item 190

  191. Checklist item 191

  192. Checklist item 192

  193. Checklist item 193

  194. Checklist item 194

  195. Checklist item 195

  196. Checklist item 196

  197. Checklist item 197

  198. Checklist item 198

  199. Checklist item 199

  200. 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:

  1. Hash queries at build; publish allow‑listed ops
  2. CDN cache by path+hash; vary only by vars that affect response
  3. 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)

  1. Identify under/over‑fetching hotspots
  2. Model minimal schema; add dataloaders; persisted queries
  3. Teach clients partial error handling; iterate

GraphQL → REST (public)

  1. Extract stable read models; strong caching; OpenAPI + SDKs
  2. 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.

Related posts