GraphQL — Query Language for APIs
GraphQL is a query language for APIs where the client describes the exact shape of the data it wants, and the server returns exactly that — no more, no less. It solves the two chronic REST problems — over-fetching (you GET a giant user object when you needed just the name) and under-fetching (you fetch a user, then their posts, then their comments — three round trips). GraphQL trades server-side complexity for client-side flexibility and performance.
Foundational.
How it works
GraphQL flips the API contract inside out. In REST, the server defines resources and their shapes; the client fetches whatever the server exposes. In GraphQL, the server defines a schema of types and fields, and the client writes a query specifying exactly which fields it wants. The server resolves those fields and returns exactly that shape.
The four building blocks:
- Schema: a strongly-typed definition of the data graph. Types, fields, relationships. Written in SDL (Schema Definition Language) or generated from code.
- Queries: read operations. The client selects fields and nested relationships.
- Mutations: write operations. Same shape, but the server executes them sequentially (queries can run in parallel).
- Subscriptions: long-lived queries that stream results over a WebSocket when underlying data changes.
There is one endpoint — typically POST /graphql — and every operation is a POST with a query string in the body.
Under the hood, GraphQL runs a resolver function for every field in the query. The resolver for user(id: 42) might call the users service. The resolver for posts on that user calls the posts service. The resolver for comments on each post calls the comments service. GraphQL assembles the response by walking the query tree and invoking resolvers.
This is where GraphQL's biggest pitfall lives: the N+1 problem. If a query asks for an author's name on each of 50 comments, a naive resolver issues 50 separate calls to the users service. GraphQL's default execution model is per-field, so without batching this becomes catastrophic.
The fix is DataLoader (and equivalents). DataLoader batches per-request: instead of fetching one user at a time, it collects all the user IDs requested during a single tick of execution, deduplicates them, and issues one bulk fetch. With DataLoader, the 50-comment author resolution becomes one call: getUsers([1, 1, 7, 12, ...]). This is essential — a GraphQL server without batching is unusable at scale.
The other performance concern is query depth and complexity. A query like user { friends { friends { friends { ... } } } } can walk the entire graph. Production GraphQL servers enforce a max depth, a complexity budget (each field costs points; queries over budget are rejected), and rate limits per client. Without these, a malicious query can DoS the server.
GraphQL shines when:
- You have many clients with different needs (web wants more fields than mobile, an Apple Watch app wants almost nothing). Each client writes its own query; the server stays simple.
- You are aggregating multiple backend services. GraphQL acts as a federation layer — the schema describes the unified graph, resolvers call the underlying microservices. This is the BFF (Backend For Frontend) pattern.
- Latency matters and round trips are expensive (mobile, satellite, international).
GraphQL hurts when:
- You have a simple, stable API with one client. REST is simpler to operate; GraphQL's runtime complexity is not worth it.
- Caching matters more than flexibility. REST gets HTTP caching for free (CDN, browser, reverse proxy); GraphQL POSTs are uncachable without Apollo's persisted queries or GET-with-hash tricks.
- You need binary efficiency. gRPC + Protobuf is much smaller on the wire than GraphQL's JSON.
- Streaming or file upload is core to the product. GraphQL subscriptions are awkward; file uploads are bolt-on.
- You cannot afford a stateful schema gateway. REST + a thin reverse proxy is operationally simpler.
The realistic pattern at many companies: REST for simple public APIs (third-party integrators want predictable URLs), gRPC for internal service-to-service (efficient, typed), GraphQL for the client-facing BFF (flexibility, mobile round-trip reduction). They coexist — GraphQL is not a REST replacement, it is a tool for a specific problem.
A single GraphQL server resolving every field becomes a monolith. Apollo Federation (and similar) lets multiple services each own a slice of the schema; a gateway stitches them into one graph at query time. Each service exposes its types and resolvers; the gateway routes field requests to the owning service. This is how large companies (Airbnb, Shopify) scale GraphQL without a monolith.
GraphQL's schema is the contract, and how it evolves shapes the system's longevity. Three rules govern schema evolution in practice.
1. Adding fields is always safe. New fields are additive — old clients do not query them, so they are not loaded, not serialized, not broken. This is the primary way GraphQL schemas grow.
2. Removing or renaming fields is breaking. Existing clients query those fields; their queries return errors. The standard migration is three phases: deprecate the field (with a @deprecated directive that introspection surfaces), wait for client usage to drop to zero (tracked via field-level metrics), then remove. In practice, removals take months to years for public APIs.
3. Changing a field's type is breaking. Going from String to Int breaks every client reading that field. The migration is to add a new field with the new type, deprecate the old, and migrate clients.
This is why GraphQL schemas feel verbose: the cost of a breaking change is high, so teams add many small, specific fields instead of one polymorphic one. The same pattern exists in Protobuf (field numbers, never reuse) and Thrift (field identifiers). The discipline is the same: design the schema to grow additively, instrument field usage so you know when something is safe to remove, and never break the contract silently.
Schema governance becomes its own concern at scale. Tools like Apollo Studio track field usage across all clients, compute the blast radius of a proposed change, and warn before a deploy. Without this tooling, teams ship breaking changes by accident and break mobile clients that take weeks to update through app store review.
A mobile app needs to render a user profile screen showing the user's name, their last 5 posts' titles, and the last 3 comments on each. Today this takes 5 REST calls. What does GraphQL change?
Pick one answer.
Your GraphQL server is slow on a query that fetches a list of 100 posts and the author name for each. What is the most likely cause and the standard fix?
Pick one answer.
Which is a real disadvantage of GraphQL compared to REST?
Pick one answer.
Engineering mental model
Mental model. Think of GraphQL — Query Language for APIs as a deliberate boundary in a system. The boundary exists because something becomes harder to manage when everything is done in one place: latency, scale, failure isolation, consistency, cost, or team ownership. The useful question is not “what does GraphQL — Query Language for APIs mean?” but “what pressure makes this boundary worth introducing, and what new failure mode does it create?”
Before choosing GraphQL — Query Language for APIs, name the workload, the critical user path, the dominant bottleneck, the failure you are trying to absorb, and the trade-off you are willing to accept. If you cannot name those five things, the design is probably premature.
curl -i https://api.example.com/v1/graphql
# Look for:
# - status code
# - latency
# - retryability
# - response sizeBack-of-the-envelope reasoning
Numerical lens: write down traffic, payload size, read/write ratio, peak multiplier and durability target before choosing a component. The numbers should justify the architecture.
Interactive thought experiment: GraphQL — Query Language for APIs
Change the variables below and predict what breaks first in GraphQL — Query Language for APIs. The production lab can later reuse these same inputs.
Change one variable at a time. Predict the failure mode first, then move the slider and see whether your mental model matches the simplified system response.
If you are stuck on GraphQL — Query Language for APIs, start by drawing the request path and marking every network hop, stateful component, queue, cache and failure boundary. Then estimate where the system will saturate.
You increase traffic by 10× in a system using GraphQL — Query Language for APIs. What should you inspect first?
Pick one answer.
Which statement is the safest engineering habit when using GraphQL — Query Language for APIs?
Pick one answer.
You have dashboards for traffic, latency, errors and saturation. You can change the architecture, but every change has operational cost.
Production scenario: your system uses GraphQL — Query Language for APIs, traffic suddenly spikes, and p99 latency doubles. What is your first move?
Interview drill
Answer this without notes: When would you choose GraphQL — Query Language for APIs, and when would you intentionally avoid it? Mention at least one bottleneck it addresses, one failure mode it introduces, and one alternative. Then quantify the workload you are designing for.
A useful engineering lens for GraphQL - Query Language for APIs: define the problem it solves, the simpler design that fails first, the constraint that forces you to introduce this concept, and the new failure modes the concept creates.
Numerical sanity check
Back-of-the-envelope reasoning beats fake precision. State your traffic, payload, concurrency and growth assumptions explicitly, then calculate enough to know whether the current architecture is orders of magnitude away from the target.
Do not optimize for a memorized definition. Reason from the workload and failure mode.
Imagine the simplest version of a system using GraphQL - Query Language for APIs. What breaks first as traffic grows by 10×, and what would you change before reaching 100×?
Pick one answer.
What you gain, what you pay
- +Eliminates over-fetching and under-fetching — the client gets exactly the shape it asks for.
- +One round trip for arbitrarily nested data — huge win on mobile.
- +Strongly-typed schema with introspection — tooling generates client code automatically.
- +Schema federation lets multiple services own slices of one graph.
- +Client-driven evolution — new fields are additive; clients adopt when ready.
- −Server complexity: resolvers, DataLoader, query complexity analysis, depth limits.
- −HTTP caching breaks — POST queries are uncachable without persisted-query tricks.
- −N+1 problem if not carefully managed with batching.
- −Security surface: arbitrary queries can DoS the server without depth/complexity limits.
- −Operationally heavier than REST — you run a query runtime, not a thin controller.
How this breaks in production
- Unbounded query depth (`user { friends { friends { ... } } }`) exhausts server resources.
- N+1 resolver calls turn a 100-item list into 100 backend calls.
- Expensive fields (image rendering, aggregation) called frequently without a complexity budget.
- Single GraphQL endpoint becomes a stateful single point of failure.
- Schema sprawl — deprecating fields across many clients is harder than versioning a REST URL.
Don't fall into these traps
- •Treating GraphQL as a database — it is a query layer in front of services and storage.
- •Forgetting DataLoader and shipping an N+1-prone server.
- •No query depth limit or complexity budget — first malicious query takes the server down.
- •Exposing every internal field — GraphQL's introspection leaks your schema; gate it.
- •Building a GraphQL gateway without a federation strategy — the gateway becomes a monolith.
- •Expecting REST-style caching to work — it does not, plan for persisted queries.
Real systems using this
How real systems implement this
- GitHub API v4 — Replaced the REST v3 API for complex queries. Clients fetch exactly the repository/issue/PR fields they need in a single call; rate limits enforced via a token budget that scales with field complexity.
- Shopify Storefront API — GraphQL-first storefront API for merchant-built storefronts. Merchants' clients (often embedded in browsers or apps) write queries tailored to the storefront they are rendering — no over-fetching regardless of store size.
- Apollo Federation at Airbnb — Multiple services each own a slice of a unified schema; an Apollo gateway stitches them at query time. Each service evolves independently while clients see one graph.
Practice saying it out loud
- Q1REST vs GraphQL vs gRPC — when would you choose each? Give a real example of each.
- Q2What is the N+1 problem in GraphQL, and how does DataLoader solve it?
- Q3How would you secure a public GraphQL API against malicious queries?
- Q4A mobile app makes 5 round trips per screen and you are asked to make it faster. Do you reach for GraphQL? What else would you consider?
- Q5How does caching work (or fail to work) with GraphQL, and how do persisted queries fix it?
Further reading & references
Core explanations are original NO CAP material. External references are provided for deeper study and standards.
What next?
Mark as understood once the mental model clicks.
Next recommended
RPC — Remote Procedure Call