GraphQL Mutations: Complete Guide to Modifying Data

Master creating, updating, and deleting data with GraphQL mutations

Published: January 2025 • 13 min read

While queries fetch data in GraphQL, mutations are how you modify it. Think of mutations as the POST, PUT, PATCH, and DELETE operations of REST APIs, but more flexible and type-safe.

This comprehensive guide covers everything from basic CRUD operations to advanced patterns like optimistic updates and batch mutations. Use our GraphQL formatter to beautify your mutations and schema validator to check your schemas. For more examples, check our GraphQL examples guide.

What Are Mutations?

Mutations are GraphQL operations that change data on the server. Unlike queries which are read-only, mutations:

  • Create new records in your database
  • Update existing data
  • Delete records
  • Execute side effects like sending emails or processing payments

💡 Key Difference: Mutations are executed sequentially (one after another), while queries run in parallel. This ensures data consistency when multiple mutations are sent together.

Mutation Syntax Basics

Schema Definition

First, define your mutation in the schema:

type Mutation {
  createUser(name: String!, email: String!): User!
  updateUser(id: ID!, name: String): User!
  deleteUser(id: ID!): Boolean!
}

Mutation Query

Then call it from your client:

mutation {
  createUser(name: "John Doe", email: "[email protected]") {
    id
    name
    email
    createdAt
  }
}

CRUD Operations with Mutations

CCreate - Adding New Data

Schema Definition

input CreateProductInput {
  name: String!
  description: String
  price: Float!
  categoryId: ID!
  tags: [String!]
}

type Mutation {
  createProduct(input: CreateProductInput!): Product!
}

Mutation with Variables

Mutation:

mutation CreateProduct(
  $input: CreateProductInput!
) {
  createProduct(input: $input) {
    id
    name
    price
    createdAt
  }
}

Variables:

{
  "input": {
    "name": "Laptop",
    "description": "...",
    "price": 999.99,
    "categoryId": "tech",
    "tags": ["electronics"]
  }
}

Best Practice: Use input types to group related fields and keep mutations clean.

RRead - Use Queries (Not Mutations)

For reading data, always use queries, not mutations. Queries can run in parallel and are cacheable. See our GraphQL tutorial for query examples.

UUpdate - Modifying Existing Data

Partial Updates

input UpdateProductInput {
  name: String
  description: String
  price: Float
  tags: [String!]
}

type Mutation {
  updateProduct(
    id: ID!
    input: UpdateProductInput!
  ): Product!
}

# Usage
mutation UpdateProduct {
  updateProduct(
    id: "123"
    input: {
      price: 899.99
      name: "Gaming Laptop"
    }
  ) {
    id
    name
    price
    updatedAt
  }
}

Tip: Make update input fields optional so clients can update only what changed.

DDelete - Removing Data

Simple Delete

type Mutation {
  deleteProduct(id: ID!): Boolean!
}

mutation DeleteProduct($id: ID!) {
  deleteProduct(id: $id)
}

Delete with Confirmation Response

type DeleteResponse {
  success: Boolean!
  message: String
  deletedId: ID
}

type Mutation {
  deleteProduct(id: ID!): DeleteResponse!
}

mutation DeleteProduct {
  deleteProduct(id: "123") {
    success
    message
    deletedId
  }
}

Security: Always implement authorization checks in your resolvers before deleting data!

Advanced Mutation Patterns

1. Batch Mutations

Create or update multiple items at once:

type Mutation {
  createProducts(inputs: [CreateProductInput!]!): [Product!]!
  updateProducts(updates: [UpdateProductBatch!]!): [Product!]!
}

input UpdateProductBatch {
  id: ID!
  input: UpdateProductInput!
}

mutation CreateMultiple {
  createProducts(inputs: [
    { name: "Laptop", price: 999 },
    { name: "Mouse", price: 29 },
    { name: "Keyboard", price: 79 }
  ]) {
    id
    name
  }
}

2. Nested Mutations

Create related data in a single mutation:

input CreateOrderInput {
  userId: ID!
  items: [OrderItemInput!]!
  shippingAddress: AddressInput!
}

input OrderItemInput {
  productId: ID!
  quantity: Int!
}

input AddressInput {
  street: String!
  city: String!
  zipCode: String!
}

mutation CreateOrder {
  createOrder(input: {
    userId: "user123"
    items: [
      { productId: "prod1", quantity: 2 }
      { productId: "prod2", quantity: 1 }
    ]
    shippingAddress: {
      street: "123 Main St"
      city: "New York"
      zipCode: "10001"
    }
  }) {
    id
    total
    items {
      product { name }
      quantity
    }
  }
}

3. Optimistic Response Pattern

Return the expected result immediately for better UX:

// Client-side (Apollo Client example)
const [likePost] = useMutation(LIKE_POST, {
  optimisticResponse: {
    likePost: {
      __typename: "Post",
      id: postId,
      likes: currentLikes + 1,
      likedByMe: true
    }
  }
});

// UI updates instantly, then syncs with server

4. Upsert Pattern

Create if doesn't exist, update if it does:

type Mutation {
  upsertProduct(
    id: ID
    input: ProductInput!
  ): Product!
}

mutation UpsertProduct {
  upsertProduct(
    id: "123"  # Optional: omit to create new
    input: {
      name: "Updated Laptop"
      price: 899.99
    }
  ) {
    id
    name
    price
  }
}

Error Handling in Mutations

Response Union Pattern

Return either success or error types:

type CreateProductSuccess {
  product: Product!
}

type ValidationError {
  field: String!
  message: String!
}

type CreateProductError {
  errors: [ValidationError!]!
}

union CreateProductResult = 
  CreateProductSuccess | CreateProductError

type Mutation {
  createProduct(
    input: CreateProductInput!
  ): CreateProductResult!
}

mutation CreateProduct($input: CreateProductInput!) {
  createProduct(input: $input) {
    ... on CreateProductSuccess {
      product {
        id
        name
      }
    }
    ... on CreateProductError {
      errors {
        field
        message
      }
    }
  }
}

Standard Error Response

type MutationResponse {
  success: Boolean!
  message: String
  errors: [Error!]
  data: Product
}

type Error {
  field: String
  message: String!
  code: String
}

mutation CreateProduct {
  createProduct(input: {...}) {
    success
    message
    errors {
      field
      message
      code
    }
    data {
      id
      name
    }
  }
}

Mutation Best Practices

✅ Return Updated Data

Always return the modified data so clients can update their cache.

✅ Use Input Types

Group related arguments into input types for cleaner APIs.

✅ Make Fields Nullable

In update mutations, make fields optional so partial updates work.

✅ Validate Input

Check data validity in resolvers before modifying the database.

✅ Handle Errors Gracefully

Return user-friendly error messages, not internal errors.

✅ Use Transactions

Wrap related database operations in transactions for consistency.

❌ Don't Use for Reads

Mutations are for writes only. Use queries for reading data.

❌ Avoid Generic Mutations

Create specific mutations instead of one "update" for everything.

Complete Example: User Registration

Here's a complete user registration mutation with validation, error handling, and best practices:

Schema Definition

input RegisterUserInput {
  email: String!
  password: String!
  name: String!
}

type RegisterUserSuccess {
  user: User!
  token: String!
}

type RegisterUserError {
  errors: [ValidationError!]!
}

union RegisterUserResult = 
  RegisterUserSuccess | RegisterUserError

type Mutation {
  registerUser(
    input: RegisterUserInput!
  ): RegisterUserResult!
}

Resolver Implementation

const resolvers = {
  Mutation: {
    registerUser: async (parent, { input }, context) => {
      // Validate input
      const errors = [];
      
      if (input.password.length < 8) {
        errors.push({
          field: 'password',
          message: 'Password must be at least 8 characters'
        });
      }
      
      // Check if email exists
      const existingUser = await db.users.findByEmail(input.email);
      if (existingUser) {
        errors.push({
          field: 'email',
          message: 'Email already registered'
        });
      }
      
      if (errors.length > 0) {
        return {
          __typename: 'RegisterUserError',
          errors
        };
      }
      
      // Hash password
      const hashedPassword = await bcrypt.hash(input.password, 10);
      
      // Create user
      const user = await db.users.create({
        email: input.email,
        password: hashedPassword,
        name: input.name
      });
      
      // Generate JWT token
      const token = jwt.sign(
        { userId: user.id },
        process.env.JWT_SECRET
      );
      
      return {
        __typename: 'RegisterUserSuccess',
        user,
        token
      };
    }
  }
};

Client Usage

mutation RegisterUser($input: RegisterUserInput!) {
  registerUser(input: $input) {
    ... on RegisterUserSuccess {
      user {
        id
        email
        name
      }
      token
    }
    ... on RegisterUserError {
      errors {
        field
        message
      }
    }
  }
}

# Variables
{
  "input": {
    "email": "[email protected]",
    "password": "securepass123",
    "name": "John Doe"
  }
}

GraphQL Development Tools

Related GraphQL Guides

External Resources

Official Documentation

Best Practices