Skip to main content

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 mutation keyword 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?

  1. Organization: Group related arguments logically
  2. Reusability: Same input can be used across mutations
  3. Clarity: Clear distinction between input and output types
  4. 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 failure
  • ID - Returns deleted ID, or null if not found
  • DeletePayload - 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.

Practical Limitation

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 resource
  • update - Modify existing resource
  • delete / remove - Delete resource
  • add / remove - Manage relationships
  • set - Replace a value
  • toggle - Flip a boolean
  • publish / 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

ConceptDescription
MutationWrite operation that modifies server data
Input TypeStructured argument type for mutations
Sequential ExecutionMutations in same request run in order
Payload TypeResponse wrapper with data and errors
IdempotencySame result when run multiple times

What's Next?

In the next chapter, we'll explore Subscriptions - GraphQL's mechanism for real-time data updates.