Mutations: Changing Data

intermediate
12 min

Mutations: Changing Data

Queries read; mutations write. Anything that creates, updates, or deletes data in a GraphQL API goes through the Mutation root type. Mutations look almost identical to queries, but they carry different semantics and some important design conventions. This lesson covers defining mutations, shaping their inputs and payloads, and the execution guarantees the spec gives you.

Defining Mutations in the Schema

Mutations are just fields on the Mutation root, typically taking an input object and returning the affected entity:

graphql
input CreatePostInput { title: String! body: String! authorId: ID! } type CreatePostPayload { post: Post errors: [UserError!]! } type UserError { field: String message: String! } type Mutation { createPost(input: CreatePostInput!): CreatePostPayload! updatePost(id: ID!, input: UpdatePostInput!): CreatePostPayload! deletePost(id: ID!): DeletePostPayload! }

Two conventions here are worth adopting from day one:

  1. Single input argument per mutation. Adding an optional field later never breaks callers.
  2. Payload wrapper types with an errors list. Validation failures like "title too long" are expected outcomes, not exceptions; modeling them in the schema lets clients render field-level messages without parsing error strings.

Calling a Mutation

graphql
mutation NewPost($input: CreatePostInput!) { createPost(input: $input) { post { id title author { name } } errors { field message } } }

With variables { "input": { "title": "Hello", "body": "First post", "authorId": "1" } }. Notice you select fields on the result just like a query, so the client gets the fresh state of the created post, including server-generated values like id, in the same round trip. That returned data is also what client caches (Apollo, Relay, urql) use to update the UI automatically.

Serial Execution: A Real Guarantee

Here is a difference that surprises people. Top-level query fields may be resolved in parallel, but the spec requires top-level mutation fields to run serially, in order:

graphql
mutation { withdraw(amount: 100) { balance } deposit(amount: 100) { balance } }

deposit will not start until withdraw completes. That said, packing multiple writes into one operation is rare in practice; most teams send one mutation per user action.

Deletions and Idempotency

For deletes, return something useful, at minimum the deleted ID so caches can evict the object:

graphql
type DeletePostPayload { deletedPostId: ID errors: [UserError!]! }

Unlike REST, GraphQL has no HTTP-verb semantics: every operation is usually a POST. Idempotency is therefore a schema and resolver concern; if a client may retry, accept a client-generated idempotencyKey in the input.

Common Pitfalls

  • Modeling mutations as RPC grab-bags (doStuff(input: JSON)): you lose the type system, the whole point of GraphQL.
  • Returning only a Boolean: clients then need a follow-up query to refresh state. Return the mutated entity.
  • Using queries for side effects: tooling assumes queries are safe to retry, prefetch, and cache. Writes belong in mutations, always.

Key Takeaways

  • Mutations are fields on the Mutation root; syntax mirrors queries but semantics are writes.
  • Use one non-null input object per mutation and return a payload type containing the entity plus a UserError list.
  • Top-level mutation fields execute serially by spec; query fields may run in parallel.
  • Always return enough data (entities, deleted IDs) for client caches to update without refetching.

Next lesson: Resolvers Explained, where we look at the server-side functions that actually make queries and mutations run.

Mutations: Changing Data - GraphQL | CodeYourCraft | CodeYourCraft