---
title: "30 — GraphQL — Asking for Exactly the Fields You Need"
uid: graphql
tags: ["relay", "data-fetching", "api", "roadmap:frontend", "apollo", "graphql"]
excerpt: "REST lets the server decide the shape; GraphQL lets the client declare exactly the fields it wants and get precisely those back. One typed graph, one query."
date: 2026-08-12T18:35:06+0000
source: https://www.aveshina.my.id/en/blog/graphql
---

"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.

```figure
<svg viewBox="0 0 740 300" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="REST over-fetch versus GraphQL exact fetch. Left side: a client asks GET /users/42 and the server returns a large bundle of fields — id, name, email, bio, avatar, address, lastSeen — most greyed out as discarded. Right side: the same client sends one GraphQL query asking for only id and name, and the server returns a small packet with exactly those two fields.">
  <defs>
    <marker id="garrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>
    </marker>
  </defs>
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <!-- column headers -->
    <text x="185" y="24" font-size="12" font-weight="700" fill="#7f1d1d" text-anchor="middle">REST — over-fetching</text>
    <text x="555" y="24" font-size="12" font-weight="700" fill="#1e1b4b" text-anchor="middle">GraphQL — exact</text>

    <!-- LEFT: client + big bundle -->
    <rect x="40" y="40" width="100" height="40" rx="8" fill="#fee2e2" stroke="#dc2626" stroke-width="1.5"/>
    <text x="90" y="64" font-size="11" font-weight="700" fill="#7f1d1d" text-anchor="middle">client</text>

    <!-- response bundle (full) -->
    <rect x="200" y="40" width="180" height="190" rx="8" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="290" y="58" font-size="10" font-weight="700" fill="#422006" text-anchor="middle">GET /users/42 response</text>

    <!-- used fields -->
    <rect x="212" y="68" width="156" height="22" rx="4" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
    <text x="290" y="83" font-size="10.5" font-family="ui-monospace, monospace" fill="#052e16" text-anchor="middle">"name": "Ave"</text>
    <!-- wasted fields -->
    <rect x="212" y="96" width="156" height="22" rx="4" fill="#fee2e2" stroke="#dc2626" stroke-width="1" opacity="0.55"/>
    <text x="290" y="111" font-size="10" font-family="ui-monospace, monospace" fill="#7f1d1d" text-anchor="middle">"email": "…"</text>
    <rect x="212" y="124" width="156" height="22" rx="4" fill="#fee2e2" stroke="#dc2626" stroke-width="1" opacity="0.55"/>
    <text x="290" y="139" font-size="10" font-family="ui-monospace, monospace" fill="#7f1d1d" text-anchor="middle">"bio": "…"</text>
    <rect x="212" y="152" width="156" height="22" rx="4" fill="#fee2e2" stroke="#dc2626" stroke-width="1" opacity="0.55"/>
    <text x="290" y="167" font-size="10" font-family="ui-monospace, monospace" fill="#7f1d1d" text-anchor="middle">"address": {…}</text>
    <rect x="212" y="180" width="156" height="22" rx="4" fill="#fee2e2" stroke="#dc2626" stroke-width="1" opacity="0.55"/>
    <text x="290" y="195" font-size="10" font-family="ui-monospace, monospace" fill="#7f1d1d" text-anchor="middle">"lastSeen": "…"</text>
    <text x="290" y="222" font-size="10" font-style="italic" fill="#7f1d1d" text-anchor="middle">wanted 1 field, got 5+</text>

    <!-- arrows left -->
    <path d="M140,80 L198,80" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#garrow)"/>
    <path d="M198,130 L142,130" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#garrow)"/>

    <!-- RIGHT: client + small packet -->
    <rect x="410" y="40" width="100" height="40" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="460" y="64" font-size="11" font-weight="700" fill="#1e1b4b" text-anchor="middle">client</text>

    <!-- query packet -->
    <rect x="540" y="40" width="180" height="190" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="630" y="58" font-size="10" font-weight="700" fill="#1e1b4b" text-anchor="middle">query { user(id:42) {</text>
    <rect x="552" y="68" width="156" height="22" rx="4" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
    <text x="630" y="83" font-size="10.5" font-family="ui-monospace, monospace" fill="#052e16" text-anchor="middle">"name": "Ave"</text>
    <text x="630" y="108" font-size="10" font-family="ui-monospace, monospace" fill="#1e1b4b" text-anchor="middle">} }</text>
    <text x="630" y="222" font-size="10" font-style="italic" fill="#1e1b4b" text-anchor="middle">asked 1 field, got 1 field</text>

    <!-- arrows right -->
    <path d="M510,80 L538,80" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#garrow)"/>
    <path d="M538,130 L512,130" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#garrow)"/>

    <!-- bottom note -->
    <text x="370" y="270" font-size="11" fill="#64748b" text-anchor="middle" font-style="italic">server fixed shape (left) vs. client-declared shape (right)</text>
  </g>
</svg>
```

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/](https://graphql.org/learn/)

[2] GraphQL Foundation, "GraphQL," 2024. [Online]. Available: [https://graphql.org/](https://graphql.org/)

[3] Apollo GraphQL, "Schema and types," Apollo Docs, 2024. [Online]. Available: [https://www.apollographql.com/docs/graphql/schema/](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](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/](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/](https://relay.dev/)

```quiz
Q: Why does REST tend to over-fetch and under-fetch?
- The client can't decide which fields it wants, and data is split across multiple fixed endpoints
- HTTP is stateless, so responses grow over time
- REST responses are always untyped
correct: 0
explain: Each REST endpoint returns a fixed shape the server baked in, and related data is split across separate endpoints. A client wanting one field still gets the whole shape, and a screen needing nested data makes multiple round trips.

Q: A GraphQL query asks for `name` and `posts { title }`. The response contains…
- every field on User and Post, because the server returns the full type
- exactly name, and posts with only title — the query shape IS the response shape
correct: 1
explain: The client declares the shape; the server walks the graph and returns precisely those fields, nothing more.

Q: What does the `!` in `name: String!` mean in a GraphQL schema?
- The field is required to be provided by the client
- The field is non-nullable — the server always returns a value for it
correct: 1
explain: `!` marks a type as non-nullable on the output side. `String!` means the resolver will never return null for that field. (On input arguments it means the client must supply the value.)

Q: You're building a large React app where the data graph is central and you want fragments co-located with components, compiled at build time. Reach for…
- Apollo Client
- Relay
correct: 1
explain: Relay is built for data-heavy React apps and co-locates each component's data fragment, then the compiler merges them into optimized queries. Apollo is the general-purpose choice without that build-step model.

Q: The main reason a GraphQL server rejects a query like `{ userr(id: 42) { name } }` (typo) before running it is…
- the HTTP layer validates endpoint names
- the query is validated against the strongly typed schema at runtime
correct: 1
explain: The schema is a contract known to both sides. The server validates every query against it before any resolver fires, catching typos and wrong argument types with a precise error.
```
