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:
query | mutation | |
|---|---|---|
| Purpose | read data | create, change, delete |
| Root type | Query | Mutation |
| Top-level fields | may run in parallel | run strictly one by one |
| Typical names | nouns: posts, user | a verb and a noun: createPost |
| Retry and cache | safe to retry and cache | a 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:
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"):
type UserError {
field: String
message: String!
}
type CreatePostPayload {
post: Post
errors: [UserError!]!
}
type UpdatePostPayload {
post: Post
errors: [UserError!]!
}
type DeletePostPayload {
deletedPostId: ID
errors: [UserError!]!
}