Skip to main content

Class 5: Mutations

Learn how to modify data with GraphQL mutations - creating, updating, and deleting resources.

Duration: ~15 minutes | Difficulty: Beginner | Prerequisites: Class 4


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.

CharacteristicQueryMutation
PurposeRead dataWrite data
Side effectsConventionally noneHas side effects
Top-level executionFields may run in parallelTop-level fields run serially
IdempotencyResolvers required to be side-effect-free and idempotent (per spec, for non-mutation fields)May not be idempotent
CachingOperation result is cacheableOperation result not cached as a query, but the response payload is normalized into client caches
Conceptual REST analogyGETPOST, PUT, PATCH, DELETE (note: GraphQL traffic typically uses POST regardless of operation type)

Basic Mutation Structure

mutation CreateMovie {
createMovie(input: {
title: "Dune"
releaseYear: 2021
genre: SCIENCE_FICTION
directorId: "1"
}) {
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 CreateDirectorMovieAndCast {
# 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

GraphQL cannot thread the return value of one mutation field into the arguments of another in the same document. The example above shows the literal "temp" in the second mutation precisely because the spec does not define inter-field variable binding. Workflows that need true chaining (use the created director's ID to attach actors) require either separate requests or a server-side resolver that performs both operations as a single transaction.


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 {
message: String!
field: [String!]
code: UserErrorCode!
}

enum UserErrorCode {
TITLE_TOO_LONG
RELEASE_YEAR_OUT_OF_RANGE
DIRECTOR_NOT_FOUND
}

type Mutation {
createMovie(input: CreateMovieInput!): CreateMoviePayload!
}

field is a nullable path into the input, expressed as a list, because an error may point at a nested input field or apply to the input as a whole. code is an enum so the client can branch on the failure without parsing the message, and so a reworded message stays backward compatible. This is the shape graphql.org's error-handling lesson prescribes.

Notice that movie is nullable. The payload represents both success and validation-failure outcomes, so on a validation error we return userErrors populated and movie: null. If movie were Movie!, returning null would violate the schema and trigger null propagation, wiping out the userErrors you wanted the client to see.

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",
"code": "RELEASE_YEAR_OUT_OF_RANGE"
}
]
}
}
}

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 - does not name the action
movie(input: MovieInput!): Movie!
}

Noun-first names such as movieUpdate are the other sanctioned convention, not a mistake. graphql.org's naming guide puts verb-first (createMovie) and noun-first (movieUpdate, the convention Shopify uses across its public API) side by side and concludes that "Consistency matters more than which pattern you choose". Noun-first groups every mutation for one entity together when the schema is sorted alphabetically; verb-first reads more naturally for operations that do not fit CRUD. Pick one and apply it across the whole schema.

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 CreateMovieIdOnly($input: CreateMovieInput!) { createMovie(input: $input) { id } }

# ✅ Returns everything needed
mutation CreateMovieWithDetails($input: CreateMovieInput!) {
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 at the resolver/database layer - either all succeed or all fail. Note: GraphQL itself does not provide transactional semantics. The specification says only that serial execution of top-level mutation fields "ensures against race conditions during these side-effects". graphql.org's mutations lesson draws out the consequence: "serial execution of top-level Mutation fields differs from the notion of a database transaction. Some mutation fields may resolve successfully while others return errors, and there's no way for GraphQL to revert the successful portions of the operation when this happens." Your server's data layer has to enforce atomicity, typically with a database transaction wrapping the mutation's work.


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 class, we'll explore Class 6: Subscriptions - GraphQL's mechanism for real-time data updates.