7 min read

Q25 — Why does GraphQL exist — what problem does it solve that REST doesn't?


What problem did Facebook run into that REST couldn't solve?

In 2012, Facebook was rebuilding its iOS app. Their existing REST API was built for the web — desktop-sized screens, reliable WiFi, relative abundance of processing power. But mobile was different: small screens, slow cellular connections, constrained battery and CPU.

The REST API was designed around server-defined resources. GET /user/42 returned a fixed set of fields the server decided were relevant for a user — maybe 50 fields. The mobile app only needed 5 of them. The other 45 fields were fetched, serialized, transmitted over expensive cellular data, and then thrown away. On a slow 3G connection, that unnecessary data was real latency and real battery drain.

The alternative was creating dedicated mobile endpoints — GET /mobile/user/42 that returned only the 5 fields mobile needed. Facebook did this, and it worked, but now they had two codebases to maintain. Then the iPad needed different data than the iPhone. Then different versions of the app needed slightly different fields. The endpoint proliferation exploded. Every new client requirement meant a new server endpoint or a server modification.

GraphQL was Facebook's solution. It was used internally for several years before being open-sourced in 2015.


What is the core insight of GraphQL?

REST puts the server in charge of what data gets returned. The server defines the shape of every response.

GraphQL flips this: the client specifies exactly what data it needs, and the server returns exactly that — no more, no less.

A GraphQL query from the mobile app:

graphql
query {
  user(id: "42") {
    name
    avatarUrl
    followerCount
    latestPost {
      title
      publishedAt
    }
  }
}

The server returns exactly these fields. The mobile app that needs 5 fields gets 5 fields. The desktop app that needs 30 fields asks for 30 fields. Same endpoint, same server, different responses shaped by what the client asked for. No new endpoints, no server changes.


Why does REST suffer from over-fetching?

Over-fetching is getting more data than you need. GET /users/42 returns a full user object with 50 fields. You needed the name and avatar for a comment thread. The other 48 fields traveled across the network and were discarded.

At low scale, this is invisible. At high scale — millions of requests, millions of mobile devices on slow connections — it's real latency and real bandwidth cost. Over-fetching also creates a hidden coupling: the server's response shape determines what clients receive, so if a client needs to display new information, it either over-fetches hoping the field is there, or waits for a server change to add it.

GraphQL eliminates over-fetching structurally: the server can only return what was asked for.


Why does REST suffer from under-fetching (the N+1 problem)?

Under-fetching is needing more requests than a single endpoint provides. Consider displaying a list of posts, each with the author's name and avatar.

With REST:

  1. GET /posts → returns 20 posts with author_id for each
  2. For each post: GET /users/{author_id} → 20 separate requests for 20 authors

That's 21 HTTP requests to render one page. On a 100ms round-trip connection, that's over 2 seconds of serial requests — even before a single byte of post content displays. This is the N+1 problem: 1 request to get the list, then N requests for each item in the list.

REST solutions are awkward. You could add an ?include=author parameter to embed author data. But then you need ?include=author,comments,tags for different clients, and the server must know about every possible inclusion combination. You could create a bespoke endpoint GET /post-feed that returns exactly what the feed page needs — but now you're back to endpoint proliferation.

GraphQL solves N+1 structurally:

graphql
query {
  posts(limit: 20) {
    title
    publishedAt
    author {
      name
      avatarUrl
    }
  }
}

One request. The GraphQL server resolves posts, then for each post resolves author. Internally, with a tool called DataLoader, it batches all 20 author lookups into a single SELECT * FROM users WHERE id IN (1, 2, 3...) instead of 20 separate queries. One HTTP round trip, one batch DB query, done.


How does GraphQL actually work?

GraphQL has three parts: a schema, a query language, and a resolver system.

The Schema defines every type, every field, and every operation available:

graphql
type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  author: User!
}

type Query {
  user(id: ID!): User
  posts(limit: Int): [Post!]!
}

The schema is strongly typed. Every field has a declared type. ! means non-null. The schema is the contract between client and server.

The Query Language is what clients send — a declaration of exactly what they need, traversing the type graph:

graphql
query GetUserFeed {
  user(id: "42") {
    name
    posts {
      title
      author { name }
    }
  }
}

Resolvers are functions on the server that tell GraphQL how to fetch each field. The user resolver fetches a user by ID. The posts resolver on a User fetches that user's posts. GraphQL calls the resolver for each field in the query and assembles the result.

All GraphQL requests go to a single endpoint — typically POST /graphql — with the query in the request body. There is no URL-per-resource convention.


What does GraphQL's introspection give you?

GraphQL servers expose their own schema — clients can query the schema itself to discover what types and fields exist. This enables:

Self-documenting APIs: Tools like GraphiQL and Apollo Studio generate interactive documentation directly from the schema. No separate documentation to write or maintain.

Type-safe client code generation: Tools like graphql-codegen read the schema and generate TypeScript types for every query. A client developer writes a GraphQL query and gets typed response objects automatically. Rename a field on the server? The type system catches every use of the old name at compile time.


What does GraphQL not solve — and where does it make things harder?

GraphQL is not a silver bullet. It trades REST's problems for a different set.

Caching is much harder. REST uses URLs as cache keys. GET /users/42 can be cached at every layer — browser, CDN, reverse proxy — by URL. All GraphQL queries go to POST /graphql with a JSON body. HTTP caches don't look at request bodies, so every GraphQL query is effectively uncacheable at the HTTP layer. You need application-level caching (cache by query hash + variables) or persisted queries (pre-registered query IDs), both of which are more complex than URL-based HTTP caching.

Complex queries can destroy your database. A client can write a deeply nested query that joins many tables, follows many relationships, and returns enormous result sets. Nothing in basic GraphQL prevents a malicious or naive client from writing { users { posts { comments { author { posts { comments { ... } } } } } } } — an exponentially expensive traversal. You need query complexity analysis (assign cost to each field, reject queries over a threshold), query depth limits, and rate limiting by query cost rather than just request count. This is non-trivial to implement correctly.

N+1 at the database level. DataLoader solves the N+1 HTTP problem, but if resolvers aren't written carefully, you can still generate N+1 database queries. Every resolver must be DataLoader-aware, which requires discipline across the whole team.

File uploads are awkward. GraphQL is designed around JSON. Uploading a file requires either base64-encoding it in the JSON body (wasteful) or using a multipart form specification grafted onto GraphQL (complex). Most teams handle file uploads with a separate REST endpoint.

Not always the right public API. GraphQL is excellent for product APIs consumed by your own clients (mobile apps, single-page apps) where you control both sides. It's harder to use as a third-party public API: consumers must learn GraphQL's query language, there's no simple curl for quick testing, and deprecating fields requires careful schema evolution.


The mental model

code
REST problems GraphQL solves:
  ├── Over-fetching: GET /users/42 returns 50 fields, client needs 5
  │     GraphQL: client asks for exactly the 5 fields it needs
  │
  ├── Under-fetching (N+1): 1 list request + N detail requests
  │     GraphQL: one query traverses the graph, DataLoader batches DB calls
  │
  └── Endpoint proliferation: mobile needs different data than desktop
        GraphQL: same endpoint, client-defined query shape

How GraphQL works:
  Schema = type system, field definitions (server-defined)
  Query = client-specified traversal of the schema
  Resolvers = functions that fetch each field
  Single endpoint: POST /graphql

GraphQL trade-offs:
  ├── HTTP caching breaks → need application-level caching by query hash
  ├── Complex queries = expensive DB work → need complexity limits
  ├── N+1 at DB level → need DataLoader discipline
  ├── File uploads → awkward
  └── Learning curve for consumers

Choose GraphQL:
  Product APIs for your own apps (mobile, SPA)
  Rapidly evolving data requirements
  Multiple clients needing different data shapes

Choose REST:
  Public third-party APIs
  Simple CRUD with stable data shapes
  When HTTP caching is valuable

GraphQL exists because REST's server-defined response shape is the wrong default for product development: it forces either over-fetching, endpoint proliferation, or constant backend changes to serve client needs. GraphQL inverts the control — clients define what they need, servers execute it. The cost is a new layer of complexity in caching, query safety, and resolver design that REST doesn't have.