Module 04 · API fundamentals · Lesson 8 of 15
GraphQL fundamentals
A typed graph the client queries for exactly what it needs. How resolvers work, why N+1 appears and how data loaders fix it, how to stop expensive queries, and the caching you give up by leaving REST.
Why it matters
GraphQL fixes a real problem: product screens need data from several resources in shapes the backend did not predict, and REST answers with either many round trips or bloated responses. It also creates real problems: the server now accepts arbitrary queries, so cost control, N+1 and caching all become your responsibility. Knowing both sides is what makes "should we use GraphQL" an engineering answer instead of a fashion one.
The idea
GraphQL exposes a schema: types with fields, some of which reference other types, plus root Query, Mutation and optional Subscription entry points. A client sends a query naming exactly the fields it wants, nested as deep as it needs, and receives JSON in the same shape. One endpoint, usually POST /graphql, serves everything.
Resolvers and the N+1 problem
Every field has a resolver, a function that returns its value given the parent object. The engine walks the query, calling resolvers field by field. The default behaviour is per-object, so a query for 50 orders and each order's customer calls the customer resolver 50 times: one query for orders, then 50 for customers. That is N+1, and it appears in any GraphQL server that is not defended against it.
The fix is a data loader: a per-request batching layer that collects all the customer ids requested during one tick of execution and issues a single WHERE id IN (...) query, then caches results for the rest of the request. Every relationship resolver should go through one.
Protecting the server from the query
Because clients choose the shape, a hostile or careless query can request a deeply nested graph that touches millions of rows. Standard defences:
- 1
Depth and complexity limits
Reject queries deeper than N levels or whose estimated cost (sum of field weights times list sizes) exceeds a budget. Return the cost in a response header so clients learn. - 2
Pagination on every list field
Connections withfirstandaftercursors, with a maximum page size, so no field can return an unbounded list. - 3
Persisted queries
In production, clients send a hash of a pre-registered query instead of query text. The server executes only known queries, which also allows GET requests and CDN caching for them. - 4
Timeouts and per-caller rate limits
Based on cost, not request count: one expensive query should spend more budget than a hundred cheap ones.
Caching changes shape
REST caches by URL. GraphQL queries are POSTs to one URL, so HTTP and CDN caching are mostly lost. Caching moves to the client (normalized caches in Apollo or Relay that store objects by type and id) and to the server (data loader per request, plus application caches behind resolvers). Persisted queries over GET restore some edge caching for public, user-independent data.
Schema design and federation
Design the schema around what clients need to do, not around database tables. Use nullable fields sparingly and intentionally, because a null in a non-null field fails the whole parent. Deprecate fields with the @deprecated directive rather than versioning the API. When several teams own parts of the graph, federation composes their subgraphs into one gateway schema, so clients see a single API while ownership stays distributed.
Trade-offs
GraphQL gives clients precise fetching, strong typing and introspective tooling, and front-end teams velocity independent of backend releases. It costs server complexity (loaders, cost analysis, persisted queries), loses HTTP caching, makes rate limiting harder, and introduces a schema that must be governed. It fits when many clients need varied slices of a connected graph; it is overhead for a simple CRUD service or a public API meant for partners who expect REST.
In an interview
Pair the choice with its defences: "GraphQL at the edge because web, iOS and Android each need different slices of the same graph. Every relationship resolver goes through a data loader, lists are cursor-paginated, and we enforce a query cost budget with persisted queries in production." Mentioning N+1 and cost limits unprompted is the signal that you have run one.
Check yourself
A GraphQL endpoint is slow only for one screen that lists 100 posts with each author's name. Logs show 101 database queries per request. What is happening and what fixes it?