Skip to content

Writes and Real-Time

So far the graph has been read-only. We have selected fields, traversed links, and resolved values — but never changed anything. A real API has to let clients do things: post a review, archive a track, follow an artist. And once data can change, clients often want to know the moment it does. Those two needs map onto the two remaining root types: Mutation for writes, and Subscription for the stream of changes that follow.

You already met all three root types in the foundations module. Here we put the second and third to work:

  • Mutation — the write entry point. A field here represents one intentional change: create something, update it, delete it. It runs the change and then returns data describing the result.
  • Subscription — the real-time entry point. A field here opens a long-lived stream: the client subscribes once, and the server pushes a new payload every time the relevant event fires.

The mental model is a request that acts (mutation) versus a connection that listens (subscription). A mutation is one round trip — send the change, get the result back. A subscription is an open channel — one request, many results delivered over time.

flowchart LR
  subgraph Read
    Q["Query"] --> R1["one result"]
  end
  subgraph Write
    M["Mutation"] --> S["mutable store"]
    S --> R2["result of the change"]
  end
  subgraph RealTime
    Sub["Subscription"] --> Ev["event stream"]
    Ev --> P1["payload"]
    Ev --> P2["payload"]
    Ev --> P3["payload …"]
  end
Reads return once; mutations write then return; subscriptions push many payloads from a stream of events.

A mutation field is declared on the Mutation type. It usually takes arguments describing the change and returns the affected object so the client can update its view without a second read:

type Mutation {
addReview(trackId: ID!, rating: Int!, body: String): Review!
}

Calling it looks almost like a query — the difference is that it starts at mutation instead of { (the implicit query), and the server is allowed to change state:

mutation {
addReview(trackId: "t1", rating: 5, body: "On repeat all week.") {
id
rating
body
}
}

Across the next four lessons we go from “it works” to “it scales”:

  • Mutation design — naming writes by intent, keeping one mutation per logical change, and returning the affected data.
  • The input / payload pattern — wrapping arguments in a single input object and returning a structured payload so mutations can evolve and carry per-field errors.
  • Subscriptions — modelling real-time as a stream of events with subscribe and resolve, the transports that carry them, and when not to reach for them.
  • Mutation best practices — idempotency, validation, user errors versus protocol errors, chattiness, and optimistic UI.

The example below builds a tiny schema with a Mutation type, a mutable in-memory store of reviews, and a resolver that appends to it. Press Run to add a review and watch the store grow — the second query reads back exactly what the mutation wrote.

JavaScript

Notice the shape of the result. The mutation returns the single Review it created, complete with its server-assigned id — so the client never has to guess. The follow-up read proves the write landed: the store that was empty now contains exactly one review. That round trip — act, then receive the result — is the rhythm of every mutation in this module.

Which root type is the write entry point in a GraphQL schema?
How does a subscription differ from a query in terms of results?
After a mutation creates a record, why does it typically return the affected object?