Mutations

This section documents the current GraphQL specification (October 2021), plus the working draft for @defer/@stream/@oneOf, and the GraphQL-over-HTTP specification, as published at graphql.org and spec.graphql.org — which are the references these pages are written and verified against — and, for the integration pages, against Spring for GraphQL (2.0.x), Strawberry, Ariadne, and the client docs for Apollo Client / urql / Relay. No specific patch version is pinned. Some surfaces (Apollo Router/managed federation, graphql-ws internals, the Relay compiler internals, GraalVM native) are linked rather than documented in depth.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, as GraphQL tooling iterates quickly.

Queries read data; mutations are the GraphQL operation type used to write it — creating, updating or deleting records, or triggering any other server-side side effect. Aside from that intent, a mutation’s selection set is written and resolved the same way a query’s is: it is still a tree of fields returning a schema-defined shape, not an RPC call with a bespoke response format.

Mutations vs. queries: writing data

Both operation types are just root fields under their respective root type (Mutation instead of Query), and the client asks for a mutation exactly like it asks for a query, prefixed with the mutation keyword:

mutation AddStar {
  addStar(input: { starrableId: "MDEwOlJlcG9zaXRvcnkxMjM=" }) {
    starrable {
      viewerHasStarred
    }
  }
}

The convention of naming the field for the write it performs — createOrder, updateOrder, deleteOrder — rather than overloading a single generic save or write field keeps the schema self-documenting and lets each mutation declare its own input and payload shape. See graphql.org/learn/mutations for the canonical introduction to the operation type, including the server-side expectation that a mutation resolver returns the data the fields below it need in order to resolve, not just an acknowledgement.

Whether a field belongs under Query or Mutation — and how the two root types relate to Subscription — is covered in more depth in Queries and Fields.

One input object, not many scalar arguments

A query root field is often written with several independent scalar arguments (repository(owner: String!, name: String!)). Mutation fields instead take a single required input object argument, conventionally named input and typed as a dedicated <MutationName>Input type:

input CreateOrderInput {
  customerId: ID!
  lines: [OrderLineInput!]!
  discountCode: String
  clientMutationId: String
}

input OrderLineInput {
  sku: String!
  quantity: Int!
}

type Mutation {
  createOrder(input: CreateOrderInput!): CreateOrderPayload!
}

A few reasons this convention won over createOrder(customerId: ID!, lines: [OrderLineInput!]!, discountCode: String):

  • Evolvability without breaking clients. Adding a new optional field to an input object is a non-breaking change; adding a new positional-feeling argument to a field with many parameters is easy to get wrong and harder to keep readable as the list grows.

  • A stable, inspectable unit. Client-side tooling (generated types, form bindings, persisted-query variables) can target one CreateOrderInput type instead of tracking a field’s whole argument list.

  • Room for cross-cutting metadata. An optional clientMutationId field on the input (echoed back on the payload) lets a client correlate a mutation response with the request that triggered it — useful when several mutations are batched or de-duplicated in flight.

This is one of several payload-shaping conventions; the fuller picture — naming, nullability choices on the payload, and how these inputs interact with schema versioning — lives in Schema Design.

Top-level mutation fields execute serially

A query operation’s top-level fields may be resolved in parallel — a query asking for viewer and repository side by side implies no ordering dependency between the two, since reading data has no side effects to sequence. A mutation operation is different: the GraphQL specification requires that the root fields of a single mutation operation be executed serially, one after another, in the order they appear in the document, with each field’s side effect completing before the next one begins.

mutation TransferFunds {
  withdraw(input: { accountId: "A1", amount: 50 }) {
    account { balance }
  }
  deposit(input: { accountId: "A2", amount: 50 }) {
    account { balance }
  }
}

That guarantee exists precisely because mutations have side effects: if withdraw and deposit above ran concurrently, a client could not rely on the debit having committed before the credit is attempted, and two mutations that both touch the same record could race. Serial top-level execution gives the client a predictable, left-to-right ordering to build multi-step writes on, at the cost of some latency compared to a query with the same number of root fields. Note that this ordering guarantee is scoped to the top-level fields of one mutation operation — nested fields inside a single mutation’s payload, and fields across separate operations sent independently, follow the normal resolution rules. See graphql.org/learn/mutations ("Multiple fields in mutations") for the specification language behind this.

Returning the affected object in the payload

Because the client already sent the mutation and is about to receive a response, a well-designed payload returns the object(s) the mutation just changed — not merely a boolean or an id — so the client can update its local cache (Apollo Client’s normalized cache, Relay’s store, or a hand-rolled cache) directly from the mutation response instead of issuing a follow-up query to re-fetch the same data:

type CreateOrderPayload {
  order: Order
  userErrors: [UserError!]!
  clientMutationId: String
}
mutation CreateOrder($input: CreateOrderInput!) {
  createOrder(input: $input) {
    order {
      id
      status
      total
    }
    userErrors {
      field
      message
    }
  }
}

Returning the full updated object also matters for fields the server computes or defaults — a generated id, a server-assigned createdAt, a total computed from line items — values the client could not otherwise reconstruct locally after the write. Relay in particular leans on this: including the mutated object’s id (and any fields whose values changed) lets Relay’s store reconcile the mutation result with every query that already has that object cached, without any manual cache-update code. Deleting a record is the one case worth returning the id of the now-gone object rather than the object itself, so the client can still remove it from its cache.

Modelling expected errors as part of the payload

GraphQL’s top-level errors array (alongside data in every response) is meant for the same class of failures HTTP status codes cover for REST — a malformed query, a failed argument coercion, an unhandled exception, an authorization failure that prevents the request from being processed at all. It is a poor fit for expected, business-level failures a mutation can legitimately produce, such as "that discount code has expired" or "insufficient stock for this SKU": those are not bugs, the client needs to display them field-by-field next to a form, and a partial failure alongside other data the client still needs is exactly what errors was not designed to carry cleanly.

The established convention instead models expected failures in the schema itself, as ordinary data on the mutation’s payload, most often as a userErrors: [UserError!]! list (seen already in CreateOrderPayload above) or, in richer designs, as a union/interface result type where each mutation returns either a success shape or one of several named failure shapes:

type UserError {
  field: [String!]
  message: String!
}

union CreateOrderResult = CreateOrderSuccess | InsufficientStockError | InvalidDiscountCodeError

type CreateOrderSuccess {
  order: Order!
}

type InsufficientStockError {
  message: String!
  sku: String!
  available: Int!
}

type InvalidDiscountCodeError {
  message: String!
  discountCode: String!
}

type Mutation {
  createOrder(input: CreateOrderInput!): CreateOrderResult!
}

Whichever shape is chosen, the effect is the same: the outer GraphQL response is a clean, unconditional success (errors stays absent, data is fully populated), and the client branches on the payload’s own fields to decide whether the write actually succeeded. A response to the userErrors-style mutation above looks like this on the happy path:

{
  "data": {
    "createOrder": {
      "order": {
        "id": "T3JkZXI6NDIx",
        "status": "PENDING",
        "total": 129.99
      },
      "userErrors": []
    }
  }
}

and like this when a business rule rejects the write — still 200 OK, still no top-level errors, order simply null and the reason surfaced as data instead:

{
  "data": {
    "createOrder": {
      "order": null,
      "userErrors": [
        {
          "field": ["input", "discountCode"],
          "message": "Discount code 'SUMMER25' has expired."
        }
      ]
    }
  }
}

This is a design choice layered on top of, not a replacement for, the top-level errors array: a request that fails to execute at all (a validation error, a thrown exception the resolver did not anticipate, a denied authorization check) still belongs in errors, with a non-2xx-adjacent shape the client’s transport layer already knows how to treat as a hard failure. The full contrast between the two error channels — including how each maps onto Apollo Client’s and Relay’s error handling — is covered in Response and Error Handling.

  • Queries and Fields — how Query, Mutation and Subscription relate as root operation types.

  • Schema Design — naming and nullability conventions for inputs and payloads in more depth.

  • Response and Error Handling — the top-level errors array versus errors modelled in the schema, and how clients handle each.