So far we have only read data. But almost every application also has to change it: create a post, fix a typo, delete a comment. GraphQL has mutations for that. Picture a shop. A query is looking at the shop window: you look and change nothing. A mutation is the cash desk: you change something in the world (buy, check out, return) and immediately get a receipt — a confirmation of what happened. In a GraphQL mutation the "receipt" is the object you ask to be returned in the response. And the cash desk, unlike the window, serves people one at a time — we will talk about that too. In this lesson we will write mutations for CRUD on the posts of our blog.

How a mutation differs from a query

Syntactically a mutation is almost the same as a query: you write mutation instead of query, and inside there are the same fields, arguments, variables, aliases and fragments. The differences are in meaning and in one technical detail:

querymutation
Purposeread datacreate, change, delete
Root typeQueryMutation
Top-level fieldsmay run in parallelrun strictly one by one
Typical namesnouns: posts, usera verb and a noun: createPost
Retry and cachesafe to retry and cachea retry repeats the change too

The convention "a query reads, a mutation writes" is not enforced by the server: the resolver of a query could write something as well. But the whole ecosystem — clients, caches, tools — relies on it.

⚠️ Do not change data inside a query. A client may re-send a query at any moment, cache its result or run its fields in parallel — and a "harmless" read suddenly starts changing things, in an unpredictable order at that.

The first mutation: creating a post

Let's build a mutation step by step.

Step 1. The input data. A mutation usually has many parameters: a title, a text, an author. Instead of a pile of arguments they are collected into one input object — we met input types in the lesson about the schema. Creating a post and changing it partially need two types:

graphql
input CreatePostInput {
  title: String!
  body: String!
  published: Boolean = false
  authorId: ID!
}

input UpdatePostInput {
  title: String
  body: String
  published: Boolean
}

In CreatePostInput the required fields are marked with !, and published is false by default. In UpdatePostInput all fields are optional: the client sends only what it wants to change, and the server leaves the rest alone. The authorId field still comes from the client for now; in the authorization lesson the author will be determined from the logged-in user — you must never trust the client on the question "who am I".

Step 2. The result. What should a mutation return? It could be the Post itself, but then there is nowhere to report a validation error. That is why it is customary to return a special type — a payload ("the result of the operation"):

graphql
type UserError {
  field: String
  message: String!
}

type CreatePostPayload {
  post: Post
  errors: [UserError!]!
}

type UpdatePostPayload {
  post: Post
  errors: [UserError!]!
}

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