30 — GraphQL — Asking for Exactly the Fields You Need
"Just another way to call an API" folded GraphQL into REST's shape, which is where the confusion started. The line that everything else hangs off: unlike REST, where the server decides the shape, GraphQL lets the client declare exactly the fields it wants, and the server returns precisely that — nothing more, nothing less. [1]
The framing that finally landed for me is the contrast, not the technology in isolation. REST exposes fixed endpoints, each returning a fixed shape. The server decided years ago what a /user response looks like, and every client since has lived with that decision. GraphQL flips the authority: it exposes one endpoint that holds a typed graph (a map of the data, with every field's type declared) of everything the API knows about, and the client writes a query describing the shape of the data it wants. The server walks the graph and returns only those fields [1][2].
The REST problem GraphQL was built to solve
The thing I had to see clearly is why fixed shapes cause pain. Two failure modes keep showing up under REST:
- Over-fetching. I need a user's name for a profile chip, but GET /users/42 comes back with their bio, avatar, address, last-seen, and 30 other fields I'll never render. I paid for bytes I threw away.
- Under-fetching. I need a user and their three recent posts. GET /users/42 returns the user but no posts, so I make a second request to GET /users/42/posts. One screen, two round trips, two more if I also want comments.
Both are symptoms of the same root cause: the server baked the shape in, and my screen's needs don't line up with it. The mobile chip wants two fields; the settings page wants forty. REST gives both of them the same forty.
That diagram is the whole thesis of GraphQL in one picture. The query is the shape of the response [1].
One endpoint, one typed graph
Mechanically, GraphQL sits behind a single endpoint — usually /graphql — that accepts one HTTP POST carrying a query string. The query is a tree of fields the client wants, and the response is JSON with the exact same tree, populated [1][2]. A minimal query reads almost like the JSON it returns:
query {
user(id: 42) {
name
posts(last: 3) {
title
}
}
}The server resolves user, then posts underneath it — the nested structure maps to the graph's edges. One round trip replaced what would have been two REST calls, and only name, posts, and title came back. The mobile chip and the settings page can each ask for exactly the slice they render.
The authority for that graph lives in a schema — a declaration of every type the API exposes and the relationships between them [1][3]:
type User {
id: ID!
name: String!
posts(last: Int): [Post!]!
}This is the second idea I had to take seriously: the schema is a contract, strongly typed and known to both sides before a single query runs. ! means non-nullable; [Post!]! means a non-null list of non-null posts. The server validates every query against this schema at runtime — a typo (nam instead of name) or a wrong argument type is rejected before any resolver fires, with a precise error pointing at the field [1]. That compile-time-style safety over the wire is the part REST never gave me; with REST I was guessing field names from docs that drifted from the code.
Resolvers do the walking
The schema declares the shape; resolvers fetch the actual data. Each field on each type can have a resolver function that knows how to produce that field's value [1]. The server walks the query tree, calling resolvers as it goes — user resolver hits the users table, posts resolver hits the posts table, and so on. The client never knows or cares whether a field came from Postgres, a microservice, or a third-party API; the graph abstracts all of that into one uniform surface. That uniformity is genuinely useful, and it's also where GraphQL's real complexity hides — N+1 queries (asking for a list, then one extra query per item), authorization at every field, and schema governance all become the team's problem [4].
The clients: Apollo and Relay
Writing raw fetch('/graphql') calls works, but the moment data needs caching, loading states, or optimistic updates (updating the screen immediately and syncing in the background), a dedicated client library earns its keep. The roadmap points at two, and they sit at very different spots on the complexity curve [5][6].
Apollo Client is the general-purpose default — a framework-agnostic (with strong React bindings) library that handles caching, queries, mutations, and loading/error state [5]. Its normalized cache (a store that keeps one copy per entity, keyed by ID) deduplicates entities across queries, so fetching the same User in two places returns one cached object. For most apps, including my own work, Apollo is the "pick this one" answer: it ships everything I need, is well-documented, and doesn't dictate how I structure my components.
Relay is Meta's own client, built specifically for data-heavy React applications [6]. Its defining trait is co-location: each component declares the exact fragment (a reusable slice of a query) of data it needs, and the Relay compiler stitches those fragments into optimized queries at build time. The component's data requirements travel with the component. The payoff is efficiency at scale — Relay can aggressively prune queries, batch them, and update caches with minimal overhead — but the cost is a stricter way of thinking and a mandatory build step. Relay is the choice when "thousands of components hitting one big graph" is the actual problem; for a CRUD app (create-read-update-delete — a plain forms-and-tables app) it's overkill.
The rule of thumb I use: Apollo when I want GraphQL's convenience without rearchitecting my app, Relay when the data graph is the app and I'm willing to let the compiler shape my components.
How I use this
The practical takeaway is a decision check, not a tooling recommendation. When I reach for an API design, I ask whether the clients are varied — a mobile chip and a desktop table wanting different slices of the same entity, screens needing nested data across what would be multiple REST round trips. If yes, the client-declared shape pays for itself. If the clients are uniform and the shapes are stable, REST's fixed endpoints are simpler and I don't force GraphQL in just because it's newer. The schema-as-contract is the part I keep regardless — even in REST projects, I now reach for a typed schema (and code generation from it) to get the validation GraphQL gave me by default.
References
[1] GraphQL Foundation, "Introduction to GraphQL," graphql.org, 2024. [Online]. Available: https://graphql.org/learn/
[2] GraphQL Foundation, "GraphQL," 2024. [Online]. Available: https://graphql.org/
[3] Apollo GraphQL, "Schema and types," Apollo Docs, 2024. [Online]. Available: https://www.apollographql.com/docs/graphql/schema/
[4] Shopify Engineering, "Why and how we built the GraphQL Design Tutorial," Shopify Engineering Blog, 2022. [Online]. Available: https://shopify.engineering/how-to-tackle-the-graphql-data-graph
[5] Apollo GraphQL, "Get started with Apollo Client," Apollo Docs, 2024. [Online]. Available: https://www.apollographql.com/docs/react/
[6] Meta, "Relay — A JavaScript framework for building data-driven React applications," relay.dev, 2024. [Online]. Available: https://relay.dev/
Knowledge check · Question 1 of 5
Why does REST tend to over-fetch and under-fetch?
Comments
Leave a Comment
You must be signed in to comment
0 Comments
No comments yet. Be the first to comment!