Skip to main content

Introduction

GraphQL mutations allow you to modify data in your EverShop application. Unlike queries which are read-only, mutations perform write operations such as creating, updating, or deleting data.

Mutation Structure

Mutations in EverShop follow the standard GraphQL mutation syntax:
mutation MutationName {
  mutationField(argument: value) {
    field1
    field2
  }
}

Defining Mutations

Mutations are defined in your GraphQL schema files alongside queries:
type Foo {
  id: ID!
  name: String!
  description: String
}

type Mutation {
  createFoo(name: String!, description: String): Foo
  updateFoo(id: ID!, name: String, description: String): Foo
  deleteFoo(id: ID!): Boolean
}

Mutation Examples

Create Mutation

Create a new resource in the system.
name
String!
required
The name of the new Foo object
description
String
Optional description for the Foo object
createFoo
Foo
Returns the newly created Foo object
mutation CreateNewFoo {
  createFoo(name: "New Foo", description: "A newly created object") {
    id
    name
    description
  }
}

Update Mutation

Update an existing resource.
id
ID!
required
The ID of the Foo object to update
name
String
New name for the object (optional)
description
String
New description for the object (optional)
updateFoo
Foo
Returns the updated Foo object
mutation UpdateFoo {
  updateFoo(id: "1", name: "Updated Foo") {
    id
    name
    description
  }
}

Delete Mutation

Remove a resource from the system.
id
ID!
required
The ID of the Foo object to delete
deleteFoo
Boolean
Returns true if the deletion was successful
mutation DeleteFoo {
  deleteFoo(id: "3")
}

Input Types

For complex mutations, use input types to group related fields:
input CreateFooInput {
  name: String!
  description: String
  category: String
  tags: [String!]
}

type Mutation {
  createFoo(input: CreateFooInput!): Foo
}

Mutation Context

Mutations have access to the same context object as queries:
export default {
  Mutation: {
    createFoo: (root, { name, description }, context) => {
      // Access authenticated user
      const userId = context.user?.id;
      
      // Access database connection
      const db = context.db;
      
      // Access request object
      const headers = context.request.headers;
      
      // Implementation
      return newFoo;
    }
  }
};

Authorization

Implement authorization checks in your mutation resolvers:
export default {
  Mutation: {
    createFoo: (root, { name, description }, context) => {
      // Check if user is authenticated
      if (!context.user) {
        throw new Error('Authentication required');
      }
      
      // Check if user has permission
      if (!context.user.hasPermission('create_foo')) {
        throw new Error('Insufficient permissions');
      }
      
      // Proceed with mutation
      return createNewFoo(name, description);
    }
  }
};

Validation

Validate input data before processing:
export default {
  Mutation: {
    createFoo: (root, { name, description }, context) => {
      // Validate required fields
      if (!name || name.trim().length === 0) {
        throw new Error('Name is required and cannot be empty');
      }
      
      // Validate length
      if (name.length > 100) {
        throw new Error('Name must be 100 characters or less');
      }
      
      // Validate format
      if (!/^[a-zA-Z0-9\s]+$/.test(name)) {
        throw new Error('Name can only contain letters, numbers, and spaces');
      }
      
      // Create the resource
      return createNewFoo(name, description);
    }
  }
};

Database Integration

Integrate mutations with database operations:
export default {
  Mutation: {
    createFoo: async (root, { name, description }, context) => {
      const db = context.db;
      
      try {
        // Insert into database
        const result = await db.query(
          'INSERT INTO foos (name, description) VALUES ($1, $2) RETURNING *',
          [name, description]
        );
        
        return result.rows[0];
      } catch (error) {
        throw new Error(`Failed to create Foo: ${error.message}`);
      }
    },
    
    updateFoo: async (root, { id, name, description }, context) => {
      const db = context.db;
      
      const updates = [];
      const values = [];
      let paramIndex = 1;
      
      if (name !== undefined) {
        updates.push(`name = $${paramIndex++}`);
        values.push(name);
      }
      
      if (description !== undefined) {
        updates.push(`description = $${paramIndex++}`);
        values.push(description);
      }
      
      values.push(id);
      
      const query = `
        UPDATE foos 
        SET ${updates.join(', ')} 
        WHERE id = $${paramIndex}
        RETURNING *
      `;
      
      const result = await db.query(query, values);
      
      if (result.rows.length === 0) {
        throw new Error(`Foo with id ${id} not found`);
      }
      
      return result.rows[0];
    }
  }
};

Best Practices

Use Descriptive Names

Name your mutations clearly: createProduct, updateUser, deleteOrder

Return Created Objects

Return the full object after creation/update for immediate use

Validate Input

Always validate input data before processing mutations

Handle Errors

Provide clear, actionable error messages

Use Transactions

Wrap related database operations in transactions

Check Authorization

Verify user permissions before allowing mutations

Error Handling

Mutations should provide clear error messages:
export default {
  Mutation: {
    createFoo: (root, { name, description }, context) => {
      try {
        // Validation
        if (!name) {
          throw new Error('Name is required');
        }
        
        // Business logic validation
        if (isDuplicateName(name)) {
          throw new Error(`A Foo with name "${name}" already exists`);
        }
        
        // Create the resource
        return createNewFoo(name, description);
      } catch (error) {
        // Log error for debugging
        console.error('Error creating Foo:', error);
        
        // Re-throw with user-friendly message
        throw error;
      }
    }
  }
};

Testing Mutations

Test your mutations thoroughly:
// Example test
describe('createFoo mutation', () => {
  it('should create a new Foo', async () => {
    const result = await graphql({
      schema,
      source: `
        mutation {
          createFoo(name: "Test Foo", description: "Test description") {
            id
            name
            description
          }
        }
      `,
      contextValue: { user: mockUser }
    });
    
    expect(result.data.createFoo).toBeDefined();
    expect(result.data.createFoo.name).toBe('Test Foo');
  });
  
  it('should reject creation without authentication', async () => {
    const result = await graphql({
      schema,
      source: `
        mutation {
          createFoo(name: "Test Foo") {
            id
          }
        }
      `,
      contextValue: {}
    });
    
    expect(result.errors).toBeDefined();
    expect(result.errors[0].message).toContain('Authentication required');
  });
});

Next Steps

GraphQL Queries

Learn about querying data with GraphQL

GraphQL Overview

Review GraphQL architecture and concepts