GraphQL

Mutations in GraphQL

17 min Lesson 4 of 35

Understanding GraphQL Mutations

While queries are used to fetch data, mutations are used to modify data on the server. Mutations allow you to create, update, or delete data. Just like queries, mutations can return data, which is useful for getting the updated state after an operation.

Key Concept: Mutations are similar to queries in syntax, but they signal to the server that you intend to modify data. By convention, any operation that causes side effects should be a mutation, not a query.

Basic Mutation Syntax

Here's a simple mutation to create a new user:

# Schema definition type Mutation { createUser(name: String!, email: String!): User! } # Mutation request mutation { createUser(name: "Ahmed Hassan", email: "ahmed@example.com") { id name email createdAt } } # Response { "data": { "createUser": { "id": "456", "name": "Ahmed Hassan", "email": "ahmed@example.com", "createdAt": "2026-02-16T10:30:00Z" } } }

Notice that the mutation returns the created user, allowing the client to immediately access the new data without making a separate query.

Mutations with Variables

Like queries, mutations should use variables for dynamic values:

mutation CreateUser($name: String!, $email: String!, $age: Int) { createUser(name: $name, email: $email, age: $age) { id name email age createdAt } } # Variables { "name": "Ahmed Hassan", "email": "ahmed@example.com", "age": 28 }
Best Practice: Always use variables with mutations. This prevents injection attacks and allows for better query caching and validation.

Input Types for Complex Arguments

When mutations have many arguments, use input types to group related data:

# Schema with input type input CreateUserInput { name: String! email: String! age: Int bio: String avatar: String } input UserAddressInput { street: String! city: String! country: String! postalCode: String! } type Mutation { createUser(input: CreateUserInput!, address: UserAddressInput): User! } # Mutation with input type mutation CreateUser($input: CreateUserInput!, $address: UserAddressInput) { createUser(input: $input, address: $address) { id name email address { city country } } } # Variables { "input": { "name": "Ahmed Hassan", "email": "ahmed@example.com", "age": 28, "bio": "Full-stack developer" }, "address": { "street": "123 Main St", "city": "Cairo", "country": "Egypt", "postalCode": "11511" } }

Update Mutations

Update mutations typically require an ID to identify the record and an input object with the fields to update:

input UpdateUserInput { name: String email: String age: Int bio: String } type Mutation { updateUser(id: ID!, input: UpdateUserInput!): User! } # Mutation mutation UpdateUser($id: ID!, $input: UpdateUserInput!) { updateUser(id: $id, input: $input) { id name email age bio updatedAt } } # Variables - only update specific fields { "id": "456", "input": { "bio": "Senior Full-stack Developer", "age": 29 } }
Important: For update mutations, make input fields nullable so clients can update only the fields they want to change. Non-nullable fields would require sending all values on every update.

Delete Mutations

Delete mutations typically take an ID and return either the deleted object or a success status:

type DeleteUserPayload { success: Boolean! message: String deletedUserId: ID } type Mutation { deleteUser(id: ID!): DeleteUserPayload! } # Mutation mutation DeleteUser($id: ID!) { deleteUser(id: $id) { success message deletedUserId } } # Response { "data": { "deleteUser": { "success": true, "message": "User deleted successfully", "deletedUserId": "456" } } }

Mutation Responses

It's a best practice to return structured responses from mutations, including success status, messages, and the modified data:

type CreateUserPayload { success: Boolean! message: String user: User errors: [UserError!] } type UserError { field: String! message: String! } type Mutation { createUser(input: CreateUserInput!): CreateUserPayload! } # Mutation with validation errors { "data": { "createUser": { "success": false, "message": "Validation failed", "user": null, "errors": [ { "field": "email", "message": "Email is already taken" }, { "field": "age", "message": "Age must be at least 18" } ] } } }
Best Practice: Return detailed error information in mutation responses rather than relying solely on GraphQL errors. This gives clients more control over error handling and user feedback.

Multiple Mutations in One Request

You can execute multiple mutations in a single request. Unlike queries (which execute in parallel), mutations execute sequentially:

mutation CreateMultipleUsers { user1: createUser(input: { name: "Ahmed Hassan" email: "ahmed@example.com" }) { id name } user2: createUser(input: { name: "Sara Mohamed" email: "sara@example.com" }) { id name } user3: createUser(input: { name: "Omar Ali" email: "omar@example.com" }) { id name } }
Important: Mutations are executed in the order they appear in the request. This guarantees that user1 is created before user2, and user2 before user3. This is crucial for maintaining data consistency.

Optimistic Updates Concept

Optimistic updates improve user experience by updating the UI immediately, before the server responds. If the mutation fails, the UI reverts the change:

// Client-side pseudo-code function updateUserProfile(userId, newName) { // 1. Optimistically update UI immediately updateUIWithNewName(newName); // 2. Send mutation to server client.mutate({ mutation: UPDATE_USER, variables: { id: userId, input: { name: newName } } }) .then(response => { // 3. Server succeeded - UI already updated console.log('Update confirmed'); }) .catch(error => { // 4. Server failed - revert UI changes revertUIToOldName(); showErrorMessage(error); }); }

GraphQL clients like Apollo Client provide built-in support for optimistic updates, making this pattern easy to implement.

Mutation Naming Conventions

Following consistent naming conventions makes your API more predictable and easier to use:

type Mutation { # CRUD operations createUser(input: CreateUserInput!): CreateUserPayload! updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload! deleteUser(id: ID!): DeleteUserPayload! # Specific actions publishPost(id: ID!): PublishPostPayload! archivePost(id: ID!): ArchivePostPayload! # Bulk operations createManyUsers(inputs: [CreateUserInput!]!): CreateManyUsersPayload! deleteManyUsers(ids: [ID!]!): DeleteManyUsersPayload! # Relationship operations addPostTag(postId: ID!, tagId: ID!): AddPostTagPayload! removePostTag(postId: ID!, tagId: ID!): RemovePostTagPayload! # Authentication login(email: String!, password: String!): LoginPayload! logout: LogoutPayload! refreshToken(token: String!): RefreshTokenPayload! }

Common naming patterns:

  • create{Resource} - Create a new resource
  • update{Resource} - Update an existing resource
  • delete{Resource} - Delete a resource
  • {verb}{Resource} - Specific actions (publish, archive, approve, etc.)
  • {verb}Many{Resources} - Bulk operations
  • add{Relationship} - Create a relationship
  • remove{Relationship} - Remove a relationship

Mutation Best Practices

  • Use input types - Group related arguments for better organization
  • Return meaningful data - Include the modified resource and operation status
  • Handle errors gracefully - Return structured errors in the payload
  • Name mutations clearly - Use consistent verb-noun patterns
  • Validate input - Check data before processing mutations
  • Be idempotent when possible - Same mutation with same input should have same effect
  • Use transactions - Ensure data consistency with database transactions
Exercise: Design mutations for a blog platform with the following requirements:
  • Create, update, and delete blog posts
  • Posts have title, content, status (DRAFT/PUBLISHED), and tags
  • Add and remove tags from posts
  • Publish and unpublish posts (separate from update)
  • Create proper input types for each mutation
  • Return structured payload types with success status, messages, and errors
  • Use consistent naming conventions

Summary

In this lesson, you've learned how to create GraphQL mutations for modifying data. You've explored input types, mutation responses, multiple mutations, optimistic updates, and naming conventions. Mutations are essential for building interactive applications, and following best practices ensures your API is robust and user-friendly.