Mutations
Learn how to modify data with GraphQL mutations - creating, updating, and deleting resources.
What is a Mutation?
A mutation is a GraphQL operation that modifies data on the server. While queries are read-only, mutations have side effects - they create, update, or delete data.
┌─────────────────────────────────────────────────────────────────────┐
│ QUERIES vs MUTATIONS │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ QUERY MUTATION │
│ ───────────────────────────────────────────────────────────────── │
│ Read data Write data │
│ No side effects Has side effects │
│ Can run in parallel Run sequentially │
│ Idempotent May not be idempotent │
│ Cacheable Not cacheable │
│ │
│ REST equivalent: REST equivalent: │
│ GET POST, PUT, PATCH, DELETE │
│ │
└─────────────────────────────────────────────────────────────────────┘
Basic Mutation Structure
mutation CreateMovie {
createMovie(title: "Dune", releaseYear: 2021, genre: SCIFI) {
id
title
releaseYear
}
}
Response:
{
"data": {
"createMovie": {
"id": "42",
"title": "Dune",
"releaseYear": 2021
}
}
}
Key observations:
- The
mutationkeyword identifies the operation type - Mutations return data, allowing you to fetch the created/updated resource
- The response shape mirrors the requested fields
Mutations in the Schema
Mutations are defined in the schema under the Mutation type:
type Mutation {
"""
Create a new movie.
Returns the created movie.
"""
createMovie(input: CreateMovieInput!): Movie!
"""
Update an existing movie.
Returns the updated movie, or null if not found.
"""
updateMovie(id: ID!, input: UpdateMovieInput!): Movie
"""
Delete a movie by ID.
Returns true if deletion was successful.
"""
deleteMovie(id: ID!): Boolean!
"""
Add an actor to a movie's cast.
"""
addActorToMovie(movieId: ID!, actorId: ID!): Movie!
}
Input Types
For mutations with multiple arguments, input types provide structure and reusability:
input CreateMovieInput {
title: String!
releaseYear: Int!
genre: Genre!
rating: Float
duration: Int
summary: String
directorId: ID!
}
input UpdateMovieInput {
title: String
releaseYear: Int
genre: Genre
rating: Float
duration: Int
summary: String
}
Why Input Types?
- Organization: Group related arguments logically
- Reusability: Same input can be used across mutations
- Clarity: Clear distinction between input and output types
- Evolution: Adding optional fields is backward compatible
Create vs Update Inputs
Notice the difference:
CreateMovieInput: Required fields are non-null (title: String!)UpdateMovieInput: All fields are nullable (only update what's provided)
This is called partial updates - clients send only the fields they want to change.
Using Variables with Mutations
Like queries, mutations should use variables for dynamic data:
mutation CreateMovie($input: CreateMovieInput!) {
createMovie(input: $input) {
id
title
releaseYear
genre
}
}
Variables:
{
"input": {
"title": "Oppenheimer",
"releaseYear": 2023,
"genre": "DRAMA",
"rating": 8.9,
"duration": 180,
"directorId": "nolan-1"
}
}
Common Mutation Patterns
Pattern 1: Create
mutation CreateReview($input: CreateReviewInput!) {
createReview(input: $input) {
id
rating
comment
createdAt
movie {
title
# Updated average rating
rating
}
}
}
Best practice: Return the created object so the client can update its cache without a second request.
Pattern 2: Update
mutation UpdateMovie($id: ID!, $input: UpdateMovieInput!) {
updateMovie(id: $id, input: $input) {
id
title
rating
updatedAt
}
}
Variables:
{
"id": "42",
"input": {
"rating": 9.1
}
}
Best practice: Return the full updated object, not just the changed fields.
Pattern 3: Delete
mutation DeleteMovie($id: ID!) {
deleteMovie(id: $id) {
success
message
deletedId
}
}
Options for delete return types:
Boolean!- Simple, but no details on failureID- Returns deleted ID, or null if not foundDeletePayload- Structured response with success/error info
type DeletePayload {
success: Boolean!
message: String
deletedId: ID
}
Pattern 4: Relationship Mutations
mutation AddActorToMovie($movieId: ID!, $actorId: ID!) {
addActorToMovie(movieId: $movieId, actorId: $actorId) {
id
title
actors {
id
name
}
}
}
Sequential Execution
Unlike queries, mutations in the same request execute sequentially:
mutation SetupMovie {
# 1. First, create the director
director: createDirector(input: { name: "Denis Villeneuve" }) {
id
}
# 2. Then, create the movie (runs after director is created)
movie: createMovie(input: {
title: "Dune",
directorId: "temp" # In practice, you'd need the ID from step 1
}) {
id
}
# 3. Finally, add actors (runs after movie is created)
actor1: addActorToMovie(movieId: "temp", actorId: "actor-1") { id }
actor2: addActorToMovie(movieId: "temp", actorId: "actor-2") { id }
}
This guarantees order of operations when mutations depend on each other.
In practice, you often can't reference results from earlier mutations in the same request (like using the created director's ID). Most workflows require separate requests or server-side transaction handling.
Mutation Response Design
Return the Modified Object
# ✅ Good - returns the created movie
type Mutation {
createMovie(input: CreateMovieInput!): Movie!
}
# ❌ Less useful - only returns the ID
type Mutation {
createMovie(input: CreateMovieInput!): ID!
}
Returning the full object allows clients to:
- Update their cache immediately
- Display the result without a follow-up query
- Verify the mutation worked as expected
Payload Types for Complex Responses
For mutations that need to return additional metadata:
type CreateMoviePayload {
movie: Movie!
userErrors: [UserError!]!
}
type UserError {
field: String!
message: String!
}
type Mutation {
createMovie(input: CreateMovieInput!): CreateMoviePayload!
}
Response on success:
{
"data": {
"createMovie": {
"movie": { "id": "42", "title": "Dune" },
"userErrors": []
}
}
}
Response on validation error:
{
"data": {
"createMovie": {
"movie": null,
"userErrors": [
{ "field": "releaseYear", "message": "Must be between 1888 and 2030" }
]
}
}
}
Naming Conventions
Mutation names should be verbs that describe the action:
type Mutation {
# ✅ Good - clear actions
createMovie(input: CreateMovieInput!): Movie!
updateMovie(id: ID!, input: UpdateMovieInput!): Movie
deleteMovie(id: ID!): DeletePayload!
publishMovie(id: ID!): Movie!
archiveMovie(id: ID!): Movie!
# ❌ Avoid - unclear or noun-based
movie(input: MovieInput!): Movie!
movieUpdate(id: ID!, input: UpdateMovieInput!): Movie
}
Common prefixes:
create- New resourceupdate- Modify existing resourcedelete/remove- Delete resourceadd/remove- Manage relationshipsset- Replace a valuetoggle- Flip a booleanpublish/unpublish- Change visibility
Idempotency
An idempotent operation produces the same result whether executed once or multiple times.
# Idempotent - setting to a specific value
mutation SetMovieRating($id: ID!, $rating: Float!) {
setMovieRating(id: $id, rating: $rating) {
id
rating
}
}
# NOT idempotent - incrementing
mutation IncrementViewCount($id: ID!) {
incrementMovieViews(id: $id) {
id
viewCount # Different each time!
}
}
For non-idempotent mutations, consider:
- Idempotency keys: Client provides a unique key; server deduplicates
- Conditional mutations: Only execute if conditions are met
Best Practices
1. Use Input Types
# ❌ Too many arguments
mutation CreateMovie(
$title: String!
$releaseYear: Int!
$genre: Genre!
$rating: Float
$duration: Int
) {
createMovie(
title: $title
releaseYear: $releaseYear
genre: $genre
rating: $rating
duration: $duration
) { id }
}
# ✅ Clean input type
mutation CreateMovie($input: CreateMovieInput!) {
createMovie(input: $input) { id }
}
2. Return Enough Data
# ❌ Forces a second query
mutation { createMovie(input: $input) { id } }
# ✅ Returns everything needed
mutation {
createMovie(input: $input) {
id
title
releaseYear
createdAt
}
}
3. Handle Errors Gracefully
Design your schema to handle expected errors (validation, not found) in the response, not just as GraphQL errors.
4. Consider Atomicity
If a mutation involves multiple operations, make it atomic - either all succeed or all fail.
Summary
| Concept | Description |
|---|---|
| Mutation | Write operation that modifies server data |
| Input Type | Structured argument type for mutations |
| Sequential Execution | Mutations in same request run in order |
| Payload Type | Response wrapper with data and errors |
| Idempotency | Same result when run multiple times |
What's Next?
In the next chapter, we'll explore Subscriptions - GraphQL's mechanism for real-time data updates.