← Back to the section

Picture a profile screen in a mobile app: you need the user's name, avatar, and order count. In plain REST you hit /users/42 — and get field after field: address, phone, sign-up date, notification settings. Half the response is useless, but it still traveled down the wire. And to get the orders you have to make another request to /users/42/orders. GraphQL came out of Facebook for exactly this pain: let the client describe what data it needs and get it in a single request. Let's see how it works and what it costs.

Two REST problems: over-fetching and under-fetching

Rigid REST responses have two symmetric flaws.

Over-fetching — you get too much. An endpoint returns a fixed set of fields, and if you need only two out of twenty, the other eighteen are still serialized, sent over the network, and parsed. On a mobile connection that's noticeable.

Under-fetching — you get too little and have to make several requests. A profile screen with orders is /users/42, then /users/42/orders, then maybe /orders/{id}/items for each order. That's how a request "waterfall" is born: each step waits for the previous one, and the screen assembles slowly.

REST fights this with workarounds — special screen-specific endpoints, parameters like ?fields=name,avatar. It works, but the contract accumulates special cases. GraphQL solves both problems with one move.

The idea: one endpoint and a shape of data

In GraphQL, unlike REST, there is no pile of URLs for each resource. There is one endpoint (usually /graphql), and the client sends it a query that describes the shape of the data it needs — which fields and which nested objects to return.

You don't think "which URL to hit". You think "which slice of the data graph do I need" and draw it right in the query. The server returns JSON of exactly the same shape — nothing extra, nothing missing.

Schema and resolvers

At the core of the server is the schema: a strictly typed description of what data exists and how it's connected. This is the contract, the analogue of the .proto in gRPC — the source of truth every query is checked against.

type User {
  id: ID!
  name: String!
  avatarUrl: String
  orders: [Order!]!
}

type Order {
  id: ID!
  status: String!
  amount: Int!
}

type Query {
  user(id: ID!): User
}

The exclamation mark ! means "this field can't be null". Query is the entry point for reads.

Behind each field sits a resolver — a function that knows how to fetch that field: go to the database, call another service, compute it. The user resolver fetches the user by id; the orders resolver inside User pulls in that user's orders. The server wires resolvers along the query tree and assembles the response.

Query, mutation, subscription

GraphQL has three kinds of operations.

  • Query — a read. "Give me this data." Doesn't change state, like GET in REST.
  • Mutation — a change. Create an order, cancel one, update a profile — anything that modifies data.
  • Subscription — a subscription to events. The server pushes updates when something happens (a new message, an order status change) — usually over WebSocket.

The split matters in practice: reads can be parallelized and cached more freely, while changes are executed carefully and one at a time.

Example: query and response

That same profile screen — one query instead of three:

query {
  user(id: "42") {
    name
    avatarUrl
    orders {
      status
      amount
    }
  }
}

The response comes back in exactly the same shape — only the requested fields:

{
  "data": {
    "user": {
      "name": "Anna",
      "avatarUrl": "https://.../42.png",
      "orders": [
        { "status": "paid", "amount": 1990 },
        { "status": "shipped", "amount": 3500 }
      ]
    }
  }
}

No phone, no settings — nobody asked for them. And the orders arrived together with the user, without a second round trip to the server. Both REST pains closed with one query.

The price of flexibility

Flexibility isn't free — it shifts complexity onto the server. Before reaching for GraphQL, it's worth knowing the cost.

Caching is harder. A REST GET is cached by proxies and CDNs by URL out of the box: same address, same response. In GraphQL everything goes as a single POST to one /graphql, and every body is different — there's nothing to cache by URL. You have to cache at the application or client level, and that's noticeably more work.

The N+1 problem on resolvers. You request a list of 20 users, each with their orders. A naive orders resolver goes to the database 20 times, once per user — plus one query for the list itself. That's 21 round trips instead of two. The standard cure is a dataloader: it collects all the ids within one "tick", batches them into a single database query (WHERE user_id IN (...)), and lays the results back out. For the nature of this problem, see the article on PostgreSQL.

Load control. Since the client builds the query itself, it can build a very heavy one: deep nesting or a list of a million items. One bad query can bring the server down. So GraphQL servers add limits — maximum query depth, complexity budgets, timeouts. REST barely has this class of risk: its set of responses is fixed in advance.

Versioning through schema evolution. REST breeds /v1, /v2. GraphQL usually has no versions: the schema evolves in place — new fields are added, obsolete ones are marked @deprecated but not removed while clients still use them. Convenient, but it takes discipline: it's easy to break an old client by removing a field too soon.

Where this applies

GraphQL fits where there are many different clients with different data needs and flexible selection matters:

  • Mobile and web apps with rich screens, where each screen assembles its own combination of fields — and you want to avoid a request waterfall on a slow network.
  • Aggregating data from several sources behind one facade: the client makes one query, and the server itself visits different services and databases.
  • A fast-moving frontend: new screens ask for new slices of data without waiting for new endpoints from the backend.

Where GraphQL is not the best choice:

  • A simple CRUD API with predictable responses — REST will be simpler and cheaper, and you keep free HTTP caching.
  • Internal high-load calls between services, where speed and a strict contract matter — gRPC is more appropriate there.
  • Files, exports, streaming delivery — not GraphQL's strong suit.

Where beginners stumble:

  • They forget about N+1. The schema is pretty, the query is elegant, and under the hood there are hundreds of database round trips. A dataloader (batching) isn't optional — it's the norm for any nested lists.
  • They leave queries unrestricted. Without depth and complexity limits, one heavy client query brings the server down. Set the limits up front, not after the first incident.
  • They expect "free" caching like in REST. With a single POST to /graphql, HTTP caching doesn't work; caching has to be designed separately.
  • They delete schema fields in a hurry. A client that was requesting one breaks immediately. First @deprecated, then removal only once nobody uses it.
  • They drag GraphQL into a simple API "because it's trendy", getting resolver complexity and lost HTTP caching where REST would have been enough.

What to learn next

GraphQL is one fork in designing a contract. Alongside it is gRPC for fast internal calls and plain REST, which you should start from by default; a separate branch of REST is HATEOAS with hyperlinks in responses. The N+1 problem and its solutions are tightly tied to how the database works, and the reliability of any network calls is in the article on timeouts, retries, and idempotency. How the choice of style fits into designing a whole system is in the system design section.