GraphQL

Mutations And Input Types

Queries read; mutations write. Syntactically a mutation looks almost identical to a query, but the conventions around it — naming, input objects, payload types, and execution order — are where real API design happens. This lesson covers both the mechanics and the conventions.

A First Mutation

Mutations live on the root Mutation type, a sibling of Query. Each mutation field is an action the API offers:

mutation PostReview($input: PostReviewInput!) {
  postReview(input: $input) {
    review {
      id
      rating
      body
      album {
        title
        averageRating
      }
    }
  }
}
{
  "input": {
    "albumId": "al_301",
    "rating": 5,
    "body": "Glasswork rewards headphones."
  }
}

Two things to notice. First, the mutation returns data — you select fields on the result exactly like a query, so the client can update its UI with the new state (including the recalculated averageRating) without a follow-up read. Second, everything the mutation needs arrives in one typed input argument.

Input Object Types

Arguments to queries are usually a few scalars. Writes need structured data, and for that GraphQL has input object types — declared with input instead of type:

input PostReviewInput {
  albumId: ID!
  rating: Int!
  body: String
}

Input types are deliberately restricted compared to output types: their fields can only be scalars, enums, lists, or other input types — never object types with resolvers, and never unions. The restriction exists because inputs are plain data supplied by the client; there is nothing to resolve.

The "single non-null input argument per mutation" pattern is a convention, not a spec rule, but it is worth adopting: it keeps mutation signatures stable as fields are added, and it gives client code one object to build and validate.

Design Mutations As Verbs, Not CRUD

Root query fields are nouns (artist, album); mutation fields should be verbs that name a business action: postReview, publishAlbum, followArtist, archiveTrack. Resist collapsing everything into generic updateAlbum with a bag of optional fields — a schema whose mutations read like the domain's actual actions is easier to authorize, log, and reason about. If "publishing" and "renaming" an album have different rules and side effects, make them different mutations.

Payload Types

Rather than returning the bare entity, most mature schemas give every mutation its own payload type:

type PostReviewPayload {
  review: Review
  userErrors: [UserError!]!
}

type UserError {
  field: String
  message: String!
}

type Mutation {
  postReview(input: PostReviewInput!): PostReviewPayload!
}

The payload wrapper buys two things: room to grow (you can later add fields like moderationStatus without breaking clients), and a home for expected failures. "Rating must be between 1 and 5" is not an exceptional server error — it is a normal business outcome that belongs in typed data the client can render next to the right form field. The errors lesson later in this track builds on this split between transport-level errors and business-outcome errors.

Serial Execution

Here is the one hard semantic difference between queries and mutations. Top-level query fields may be resolved in parallel — reads do not interfere with each other. Top-level mutation fields in a single request execute in order, one after another:

mutation ReorderSetlist {
  first: moveTrack(input: { trackId: "tr_9", position: 1 }) {
    setlist { id }
  }
  second: moveTrack(input: { trackId: "tr_4", position: 2 }) {
    setlist { id }
  }
}

The server fully completes the first moveTrack before starting the second, so the second sees the first's effects. Without this guarantee, two writes touching the same data would race. Even so, prefer one mutation that expresses the whole intent (reorderSetlist(input: {orderedTrackIds: [...]})) over a sequence of small ones — a single mutation can be transactional in your resolver; a sequence cannot roll back the steps that already committed.

Idempotency And Retries

Networks fail after the server commits but before the client hears back, and clients retry. A retried postReview then creates a duplicate. GraphQL has no built-in answer; the standard approach is the same one used in REST APIs: accept a client-generated idempotency key in the input (clientMutationId or a dedicated idempotencyKey field) and have the resolver detect replays. Design this in early for any mutation that money, inventory, or notifications hang off.

What To Check

Before moving on, make sure you can:

  • write a mutation with a variable-bound input object and select fields on its result
  • explain what input types may and may not contain, and why
  • justify verb-named mutations over generic CRUD mutations
  • describe what a payload type with userErrors gives you over returning the bare entity
  • state the serial-execution guarantee for top-level mutation fields and when it still is not enough
  • explain how a retried mutation causes duplicates and how an idempotency key prevents them

What You Should Be Able To Do

After this lesson, you should be able to design the write side of a small schema: choose mutation names that match business actions, define their input and payload types, decide where expected failures go, and spot when a sequence of mutations should be redesigned as one transactional mutation.