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.
Mutations are just fields on the Mutation root, typically taking an input object and returning the affected entity:
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:
input argument per mutation. Adding an optional field later never breaks callers.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.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.
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:
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.
For deletes, return something useful, at minimum the deleted ID so caches can evict the object:
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.
doStuff(input: JSON)): you lose the type system, the whole point of GraphQL.Mutation root; syntax mirrors queries but semantics are writes.input object per mutation and return a payload type containing the entity plus a UserError list.Next lesson: Resolvers Explained, where we look at the server-side functions that actually make queries and mutations run.