← How Services Talk to Each Other
Lesson 1 of 1

GraphQL vs. REST: When Each Genuinely Wins

SoftwareIntermediate

Two Answers to the Same Question

REST and GraphQL both answer the same question - how does a client ask a server for data - and neither one is a newer or more correct answer than the other. They're different tradeoffs, and each one costs you something the other doesn't. Picking between them by which one feels more modern is how teams end up running a GraphQL server for an API with three well-defined resources and no client diversity to speak of, or bolting five endpoint variants onto a REST API that was crying out for a client-shaped query language from day one.

What REST Actually Is

REST organizes an API around resources - nouns, not actions - each addressed by its own URL, with the HTTP verb saying what you're doing to it: GET to read, POST to create, PUT or PATCH to update, DELETE to remove. /users/42 is a resource; GET /users/42 reads it, DELETE /users/42 removes it. The API's shape is fixed by whoever designed the endpoints - if a client needs a different combination of fields or resources than what an endpoint returns, its only options are fetching more than it needs, making more than one call, or asking the backend team for a new endpoint. In exchange for that rigidity, REST gets something valuable almost for free: a GET request is cacheable by URL, by design, by every browser, CDN, and proxy sitting between the client and the server, with zero extra work from anyone.

What GraphQL Actually Is

GraphQL flips the shape question around: there's one endpoint, and the client sends a query describing exactly the fields and nested relationships it wants back, in one request. The server runs each field in that query against a resolver - a function that knows how to fetch that specific piece of data - and assembles a response whose shape matches the query exactly, nothing more. The API's shape isn't fixed by the backend team's endpoint design; it's decided per request, by whoever's asking. Here's the same profile-header request done both ways, so the shape difference isn't abstract:

graphql
query {
  user(id: 42) {
    name
    avatarUrl
    posts(limit: 3) {
      title
    }
  }
}
bash
# REST needs two separate calls to assemble the same screen
curl https://api.example.com/users/42
curl https://api.example.com/users/42/posts?limit=3

The GraphQL query asks for exactly five fields across two related objects, in one request, and gets back exactly that - nothing else. The REST version needs two round trips, and each response includes every field that endpoint was designed to return, whether this particular screen needs it or not. That gap - one shaped request versus multiple full-object requests - is the entire practical difference between the two, and it's worth naming precisely instead of leaving it as a vague feeling that GraphQL is "more flexible."

The Problem REST Has That GraphQL Actually Fixes

This is GraphQL's actual origin story, not a hypothetical: Facebook built it in 2012 because their mobile News Feed had many different screens, on many different device and network conditions, all pulling from the same enormous object graph of people, posts, comments, and photos. A REST endpoint designed for the web feed over-fetched badly on a slow mobile connection; designing a new bespoke REST endpoint per screen kept multiplying backend surface area every time a new client requirement showed up. GraphQL let every one of those screens ask for exactly its own shape against one graph, without asking the backend team to ship a new endpoint first. The two failure modes it fixes have names: over-fetching, getting an entire object back when you needed two fields of it, and under-fetching, needing several sequential calls because no single endpoint returns the combination your screen needs.

A side-by-side diagram comparing REST and GraphQL loading the same profile screen. The REST side shows two separate HTTP calls (GET /users/42, then GET /users/42/posts), each returning far more fields than the screen uses - 9 unused fields fetched across 2 round trips. The GraphQL side shows one POST /graphql call with a query asking for exactly the 5 needed fields, receiving back exactly that shape in 1 round trip with 0 unused fields fetched.
Same backend, same data - the difference is entirely in how many requests it takes and how much comes back unused.

The Problem GraphQL Has That REST Doesn't

GraphQL's flexibility moves a cost from the client to the server, and the most common place that cost shows up is the N+1 problem. Say the query above resolves posts by calling a resolver that, for each post, separately fetches its author - a naive implementation runs one query to get the 3 posts, then 3 more queries, one per post, to get each author. That's 4 database round trips for what looks like one API request, and it gets worse fast: a list of 50 items each triggering their own lookup is 51 queries from a single client request, invisible in the API shape and easy to miss until it shows up as a slow endpoint in production.

javascript
// Naive: one query per post, N+1 database round trips
const posts = await db.posts.findMany({ authorId: userId });
for (const post of posts) {
  post.author = await db.users.findById(post.authorId); // N separate calls
}

// Batched with DataLoader: one call per unique key, per request tick
const authorLoader = new DataLoader(async (authorIds) => {
  const authors = await db.users.findByIds(authorIds); // 1 call, all at once
  return authorIds.map((id) => authors.find((a) => a.id === id));
});
const posts = await db.posts.findMany({ authorId: userId });
for (const post of posts) {
  post.author = await authorLoader.load(post.authorId); // batched, not N+1
}

DataLoader (or the equivalent in whatever language your GraphQL server runs) fixes the N+1 problem by batching every load call requested within the same tick into a single underlying query - it's a real fix, but it's something the backend team has to deliberately add per resolver, not something GraphQL gives you automatically. The second cost is caching: REST's GET-per-resource shape gets HTTP caching for free, but GraphQL is typically served over a single POST endpoint, and POST responses aren't cached by browsers, CDNs, or shared proxies the way GET responses are. The usual fix is persisted queries - the client sends a short hash instead of the full query text, the server maps that hash to a pre-registered query it already knows about, and that turns a POST into something that behaves like a cacheable, identifiable request again. It's a workaround for a gap GraphQL's shape creates, not something built into the protocol the way REST's caching is.

Neither the N+1 problem nor the caching gap is a reason to avoid GraphQL outright - they're the specific engineering cost of the flexibility it buys you, and both have well-established fixes. The mistake is adopting GraphQL without budgeting for either one, then discovering both in production.

Real Systems, Real Choices

GitHub is a clean real-world case study of exactly the over-fetching problem above, at real scale. Their REST API (v3) is still there and still supported, but in 2016 they shipped a GraphQL API (v4) specifically because tools built against the REST API - their own mobile apps among them - kept needing very specific, very different combinations of data across deeply interconnected resources: an issue, its comments, the linked pull request, that PR's review status, the reviewer's profile. Building that combination through REST meant either a chain of sequential calls or a growing pile of bespoke ?expand= query parameters bolted onto existing endpoints. GraphQL let each client ask for its own exact shape against one graph instead.

When Each One Genuinely Wins

REST wins when your data maps cleanly onto a small, well-defined set of resources; when a meaningful share of your traffic actually benefits from HTTP-level caching - a public content API sitting behind a CDN is the clearest case; when your clients are fairly homogeneous and mostly want the same shape of data; and when you want the simpler operational story - no resolver graph to reason about, no N+1 problem to watch for, mature tooling (OpenAPI/Swagger) that most engineers already know.

GraphQL wins when you have several genuinely different client shapes - a web app, a mobile app, a handful of different screens - all pulling from the same underlying data graph with real, measured over- or under-fetching cost between them; when you're deliberately building a single graph in front of several backend services or data sources for the frontend team's convenience; and when your team has the capacity to handle N+1 batching and query-complexity limits as a real, ongoing engineering responsibility, not an afterthought.

"We should use GraphQL, it's more modern" is exactly the wrong reason to pick it. GraphQL is older than plenty of REST APIs still being built today, and it introduces real, ongoing engineering costs - N+1 batching, query complexity limits, cache workarounds - that a resource-shaped REST API simply doesn't have to think about. Pick based on your actual client diversity and fetching pattern, not the calendar.

Further reading

  • The GraphQL SpecificationThe formal spec - the actual source of truth for what a GraphQL query, schema, and response are allowed to look like.
  • GraphQL.org: LearnThe official introductory docs - queries, mutations, schemas, and resolvers, explained from scratch.
  • Fielding's REST Dissertation, Chapter 5Roy Fielding's 2000 doctoral dissertation - the actual original source of the term "REST," for anyone who wants the real definition instead of the popularized one.
  • GitHub's GraphQL API DocsThe real production API referenced in this lesson's GitHub case study.
  • DataLoaderThe batching library referenced in the N+1 section - built by Facebook's GraphQL team specifically to solve this problem.
  • Apollo: Automatic Persisted QueriesThe persisted-queries workaround referenced in the caching section, explained in depth.