Queries, mutations and subscriptions
GraphQL defines three operation types and no others. A query reads data. A mutation writes data. A subscription receives events that the server sends over time.
The query toolbox
Chapter 07 showed the simplest possible query. This section adds the five tools that appear in almost every real query: field arguments, aliases, fragments, variables, and directives.
Field arguments: not only on the top-level field
An argument is a value you pass to a field, written in parentheses after the field name. A field accepts arguments only if the schema declares them — and the schema can declare arguments on a field at any depth, not just on the fields at the top of the query. Here comments takes an argument three levels in.
Aliases: request the same field twice
Each key in the response is the field name by default. So what happens when you need the same field twice with different arguments? Both results would want the key post. An alias renames the key: write the name you want, a colon, then the field. The response then has one key per alias, and its shape is unambiguous.
Fragments: a named selection set you can reuse
A selection set is the group of fields inside a pair of braces. A fragment gives one selection set a name so you can reuse it instead of repeating it. The list page, the detail page, and the search page all need the same post fields — write them once, then spread the fragment with ... wherever they are needed.
Post is being selected. Spread it somewhere else and validation rejects the document before execution starts. When the product team asks for a view count on every post card, you edit the fragment once.Inline fragments: selecting fields that only some types have
A field can return an interface or a union — a type that stands for several concrete types. On such a field you may only select the fields that all of those types share. To reach the fields of one specific type, open an inline fragment: a type condition with no name.
__typename is a built-in field you can select in any selection set on an object, interface, or union type. It returns the name of the type that was actually returned, so the client knows which branch it is holding. Client caches also use it: they store each object once under a key built from its __typename and its id.Variables: how dynamic values enter a query
Which post the user opens is only known at run time. The first idea is usually to build the query text with string concatenation. Do not do that. It has the same problem as building SQL by concatenation: once user input is part of the query text, it is no longer data. It can change the structure of the query.
What the extra typing buys you
Variables are declared on the operation — query GetPost($id: ID!) — and their values are sent as a separate JSON object next to the query. Three things follow. First, a value stays a value: it is never parsed as part of the query, so concatenation cannot be used to inject structure. Second, the server checks the declared type before it executes anything, so $id: ID! means a missing or wrongly typed id is rejected up front. Third, the query text is constant, so the server can parse and validate it once and cache the result, and persisted queries become possible: the client sends an identifier instead of the text (chapter 10).
Directives: @include and @skip
The mobile layout does not show comments; the desktop layout does. You do not need two query documents for that. A directive attaches to a field and changes how it is handled — here, whether it is part of the request at all.
@include is true and @skip is false.Two are guaranteed; the rest are not
@include and @skip are the two executable directives that the specification requires every server to support. You can rely on them anywhere. @deprecated is different: it is a schema directive. It marks a field as outdated inside the schema definition, and you cannot write it in a query. A server may also define its own directives, but those work only on that server. Check the schema before using one.
All five tools appear in the query below. Click any line to see which part it is and what it does.
Three things on one line. query says this operation reads. PostPage is the operation name, which is what errors, logs, and monitoring will show. The parentheses hold the variable declarations: $id must be an ID and cannot be omitted (that is the !), and $withComments is a boolean. The values are not written here — they travel with the request as a separate JSON object.
Mutations: changing data
Publishing a post, deleting a comment, adding a like — every write goes through a mutation. The syntax is almost the same as a query. Two rules are different.
The first rule is a convention: reads use query, writes use mutation. Nothing in the syntax stops you from changing data inside a query, but doing so misleads every person and every tool that reads your API. Complex input is usually collected into a single input type (chapter 08) instead of a long list of separate arguments.
createPost choose what you want to see once the write is done. The usual practice is to return the new state — the id of the new post, the createdAt the server generated — so the client can update its local cache without sending a second query.The specification: top-level mutation fields execute in series
In a query, the top-level fields may be executed in parallel. Reads do not affect each other, so the order does not change the result. In a mutation, the specification requires the top-level fields to execute one after another, in the order they are written. The first one finishes before the second one starts.
Why the difference? Writes can depend on each other. If "take money out" and "put money in" ran at the same time, the result would depend on timing. Serial execution removes that.
One detail people get wrong: this rule covers only the top-level fields of the mutation. Inside the payload of one mutation field — id, title, author above — the fields resolve like any query, and may run in parallel. And serial execution is not a transaction. If the second mutation fails, the first one is not undone. Rolling back is your server's job.
Subscriptions: receiving events over time
A query and a mutation both follow one pattern: you ask once, the server answers once. A subscription is different. You register interest once, and the server sends results whenever the event happens.
A single request-and-response exchange cannot deliver something that happens ten minutes later, because it is already finished. A subscription therefore needs a connection that stays open. The usual transport is a WebSocket: a connection, opened from the browser, that both sides can send messages on until one of them closes it. SSE (Server-Sent Events) is also used: that one is still HTTP, but the response body stays open and the server keeps writing events into it, one direction only.
subscription { newComment(postId: "1") … } and negotiates a connection that will stay open, usually a WebSocket. The subscription is registered on the server.Chat messages, the cursors of other people in a shared document, market prices, live scores. One second late is already noticeable. Polling for these is either too frequent (wasted server work) or too slow (visible lag).
A notification badge, or a dashboard that nobody minds seeing a few minutes late. Sending a query on a timer is simpler and more reliable. An open connection costs memory, heartbeat messages, and reconnection handling on both sides.
Errors: { data, errors } and partial results
This is where GraphQL differs most from REST. REST reports failure with a status code. GraphQL puts errors inside the response body, next to the data.
The specification fixes the shape of the response: a data entry and an errors entry, and both may be present at once. Each field is produced by its own resolver. When one resolver fails, the others keep running: the failed field is recorded in errors, and the fields that succeeded are still in data. This is called a partial result, and REST has no equivalent — there, one request either succeeds or fails. Click through the response below line by line.
The failure: the resolver for comments threw an error, for example because the comment service timed out. The field is set to null. The reason is not stored here — it is in the errors array below.
Partial results have one limit: non-null fields. Before the limit makes sense, you need to read the type notation correctly.
! applies to the thing immediately on its left. In [String!]! the inner ! belongs to String, and the outer one belongs to the list. Read from the inside out and the two are never confused.A ! is a promise to the client: this position will never hold null. When a resolver for a non-null field fails, the server cannot keep that promise and cannot put null there either. So the null moves up to the nearest parent field that is allowed to be null, and everything below that parent is discarded with it. The same rule was stated in chapter 07; here is what it looks like step by step.
author throws an error — say the user service is down. title is already resolved. The question is what to put in the author position.res.ok cannot tell you whether a GraphQL request succeeded
A GraphQL server traditionally answers 200 OK even when every field failed. The status code describes the HTTP exchange; the errors are in the body. So the res.ok check from chapter 02 is not enough here. Read body.errors.
The always-200 habit is changing. The GraphQL over HTTP specification is still a draft, and it allows a non-2xx status for errors that concern the whole request, such as a document that fails validation. Chapter 10 goes into it.
Pagination: Relay cursor connections
A list cannot be returned all at once. Chapter 05 answered this for REST. The GraphQL world has a widely copied convention that looks unusual at first and solves the same problem.
Three levels for a list looks like a lot. Each one has a purpose. edges is the list of connections between the parent and each item: the item itself is under node, and anything that describes the relationship sits next to it — every edge carries its own cursor, and a schema can add fields such as the date a user started following. cursor is an opaque bookmark. It looks like nonsense on purpose: it may encode a sort key or a shard position, and the server does not want clients depending on that. Do not parse it; send it back unchanged. pageInfo describes the page itself: whether there is more, and where the next page starts.
first: 3 and no after, so the server starts at the beginning. It returns P1–P3. pageInfo reports that more items exist (hasNextPage: true) and that the bookmark for the last item on this page is "c3".A convention, not syntax
Connections come from the Relay specification (Relay is Meta's GraphQL client). The GitHub GraphQL API follows it, and you will meet it in many large schemas — but it is not part of the GraphQL language. The Rick and Morty API uses plain page numbers instead: characters(page: 2), with an info.next field holding the next page number. Both are in use. Cursors handle deep lists that change while you read them, without skipping or repeating items; page numbers are simpler and let a user jump straight to page 5. The practice tasks use the page-number style.
Practice
GraphiQL is already set up for you; open it in the browser. Three tasks cover variables, fragments, aliases, and pagination.
Quiz
Eight questions. The two about partial results and non-null bubbling matter most — if you miss them, read §04 again.
You are building a chat app. ① When a user enters a room, load the last 50 messages. ② The user sends a new message. ③ When someone else sends a message, it appears immediately. Which operation type fits each step?
What problem does an alias solve?
Compared with building the query text by concatenation (`{ post(id: "${input}") }`), what does using a variable ($id: ID!) give you?
The specification requires the top-level fields of a mutation to execute in series, while the top-level fields of a query may execute in parallel. Why the difference?
Can data and errors appear in the same GraphQL response?
The schema declares author: User! (non-null), and post itself is nullable. At run time the resolver for author throws. What does the response look like?
To fetch the next page with Relay-style connections, which two pageInfo fields do you need? (Select all that apply)
A subscription lets the server send events to the client at any time. What is normally used underneath to make that possible?
- Three operation types, and no others:
queryreads,mutationwrites,subscriptionreceives events over time. Keeping reads and writes on the right operation is a convention, and other people rely on it. - The toolbox: the schema may declare arguments on a field at any depth; an alias renames a response key so the same field can be requested twice; a fragment names a selection set so it is written once; an inline fragment (
... on Dog) reaches the fields of one type behind an interface or union. - Variables are declared on the operation and sent as a separate JSON object. The query text stays constant, which prevents injection through string building, lets the server check types before execution, and makes cached parsed documents and persisted queries possible.
@include(if:)and@skip(if:)are the two executable directives every server must support. - A mutation returns the new state so the client can update its cache. Its top-level fields execute in series, in written order; the fields inside each payload resolve like a query. Serial execution is not a transaction.
- The response is always
{ data, errors }, and both may appear together — a partial result. A failing non-null field cannot holdnull, so thenullmoves up to the nearest parent that allows it. Read!as applying to the thing on its left. - A GraphQL server traditionally answers
200 OKeven on failure, sores.okproves nothing. Checkbody.errors. - For Relay pagination, watch two fields:
hasNextPagedecides whether to continue, andendCursorgoes into the next request asafter. A cursor is an opaque bookmark — do not parse it.