Mutations with GraphQL
Building on the concepts from our Querying with GraphQL guide, this guide covers how to modify data using GraphQL mutations.
Basic Mutation Structure
Simple Mutations
The most basic mutation structure:
mutation {
  createPost(input: {
    title: "Hello World"
    content: "This is my first post"
    status: PUBLISH
  }) {
    post {
      id
      title
    }
  }
}
Named Operations
Like queries, it’s best practice to use named operations:
# ✅ Better: Using the mutation keyword and operation name
mutation CreateNewPost {
  createPost(input: {
    title: "Hello World"
    content: "This is my first post"
    status: PUBLISH
  }) {
    post {
      id
      title
    }
  }
}
Using Variables
Variables are especially important in mutations to handle user input safely:
mutation CreateNewPost($input: CreatePostInput!) {
  createPost(input: $input) {
    post {
      id
      title
    }
  }
}
Variables would be passed like:
{
  "input": {
    "title": "Hello World",
    "content": "This is my first post",
    "status": "PUBLISH"
  }
}
HTTP Method Requirements
[!IMPORTANT] Mutations can only be executed using HTTP POST requests. Attempting to execute mutations over GET requests will result in an error.
// ✅ Correct: Using POST for mutations
fetch('/graphql', {
  method: 'POST',  // Required for mutations
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query: `mutation { ... }`,
    variables: { }
  })
})
Common errors when using GET requests:
{
  "errors": [
    {
      "message": "GET supports only query operation",
      "category": "request"
    }
  ]
}
[!TIP] While queries can be executed over both GET and POST requests, mutations are restricted to POST requests for security reasons and to follow proper HTTP semantics.
Authentication Requirements
[!IMPORTANT] Most mutations in WPGraphQL require authentication and proper user capabilities. Without proper authentication, you’ll receive “User is not authorized” errors.
For example, creating a post requires a user to have the publish_posts capability:
mutation CreatePost($input: CreatePostInput!) {
  createPost(input: $input) {
    post {
      id
      title
    }
  }
}
Some mutations that typically don’t require authentication:
- login: Authenticating users (provided by the WPGraphQL JWT Authentication plugin and others)
- registerUser: When registration is enabled
- createComment: When comments are open
- submitForm: For form submissions (when enabled - not a core mutation, but some extension plugins have form submission mutations that don’t require auth)
For most other mutations:
- Ensure you’re authenticated
- Verify the user has proper capabilities
- Include authentication headers with your request following documentation for whatever authentication method you’re using
[!TIP] See our Authentication and Authorization guide for details on how to authenticate your requests.
Common Authentication Errors
mutation UpdatePost($input: UpdatePostInput!) {
  updatePost(input: $input) {
    post {
      id
      title
    }
  }
}
Response when not authenticated:
{
  "errors": [
    {
      "message": "Sorry, you are not allowed to update a post",
      "category": "user"
    }
  ]
}
Common Mutation Patterns
Post Operations
Creating Posts
mutation CreatePost($input: CreatePostInput!) {
  createPost(input: $input) {
    post {
      id
      title
      status
      uri
    }
  }
}
Variables:
{
  "input": {
    "title": "My New Post",
    "content": "Post content here...",
    "status": "PUBLISH",
    "categories": {
      "nodes": [
        { "id": "category-id-here" }
      ]
    },
    "tags": {
      "nodes": [
        { "id": "tag-id-here" }
      ]
    }
  }
}
Example successful response:
{
  "data": {
    "createPost": {
      "post": {
        "id": "cG9zdDo1",
        "title": "My New Post",
        "status": "PUBLISH",
        "uri": "/my-new-post"
      }
    }
  }
}
Example error response:
{
  "errors": [
    {
      "message": "Sorry, you are not allowed to create posts",
      "category": "user"
    }
  ]
}
Updating Posts
mutation UpdatePost($id: ID!, $input: UpdatePostInput!) {
  updatePost(input: {
    id: $id,
    # Only include fields you want to update
    title: $input.title
    content: $input.content
  }) {
    post {
      id
      title
      modified # When the post was last modified
    }
  }
}
Deleting Posts
mutation DeletePost($id: ID!) {
  deletePost(input: {
    id: $id,
    # Optional: force delete instead of moving to trash
    forceDelete: true
  }) {
    # Returns the deleted post
    post {
      id
      title
    }
    # Was the post deleted?
    deleted
  }
}
[!NOTE] By default, deleting a post moves it to the trash. Use
forceDelete: trueto permanently delete.
Managing Post Meta
mutation UpdatePostMeta($id: ID!) {
  updatePost(input: {
    id: $id
    # Update custom fields
    customFields: [
      { key: "my_field", value: "new value" }
    ]
  }) {
    post {
      id
      # Query the updated meta
      customFields {
        key
        value
      }
    }
  }
}
Media Operations
Creating Media Items
[!NOTE] File uploads are not handled directly through GraphQL mutations. The file must first be uploaded to WordPress using the REST API or other methods, then you can create a MediaItem with the uploaded file’s details.
mutation CreateMediaItem($input: CreateMediaItemInput!) {
  createMediaItem(input: $input) {
    mediaItem {
      id
      title
      altText
      sourceUrl
    }
  }
}
Variables:
{
  "input": {
    "title": "My Image",
    "altText": "Description of image",
    "description": "Detailed description here",
    "filePath": "/2024/03/my-image.jpg",
    "status": "PUBLISH"
  }
}
Updating Media Items
mutation UpdateMediaItem($id: ID!, $title: String, $altText: String) {
  updateMediaItem(input: {
    id: $id
    title: $title
    altText: $altText
  }) {
    mediaItem {
      id
      title
      altText
      modified
    }
  }
}
Deleting Media Items
mutation DeleteMediaItem($id: ID!) {
  deleteMediaItem(input: {
    id: $id
    forceDelete: true
  }) {
    mediaItem {
      id
      title
    }
  }
}
[!IMPORTANT] When deleting media items, consider:
- Files will be deleted from the server when
forceDeleteis true- Referenced files in content may break if not updated
- Ensure proper backup procedures are in place
User Operations
Creating Users
mutation CreateUser($input: CreateUserInput!) {
  createUser(input: $input) {
    user {
      id
      databaseId
      username
      email
      firstName
      lastName
      roles {
        nodes {
          name
        }
      }
    }
  }
}
Variables:
{
  "input": {
    "username": "newuser",
    "email": "[email protected]",
    "firstName": "John",
    "lastName": "Doe",
    "roles": ["subscriber"],
    "password": "secure_password_here"
  }
}
Updating Users
mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
  updateUser(input: {
    id: $id
    firstName: $input.firstName
    lastName: $input.lastName
    description: $input.description
  }) {
    user {
      id
      firstName
      lastName
      description
      modified
    }
  }
}
Deleting Users
mutation DeleteUser($id: ID!) {
  deleteUser(input: {
    id: $id
    reassignPosts: null  # Optional: User ID to reassign content to
  }) {
    user {
      id
      databaseId
    }
  }
}
[!IMPORTANT] When deleting users:
- Consider what happens to their content
- Use
reassignPoststo transfer content to another user- Ensure proper user capabilities (
delete_users)- Cannot delete your own user account
Managing User Meta
mutation UpdateUserMeta($id: ID!, $input: UpdateUserInput!) {
  updateUser(input: {
    id: $id
    # Update custom fields
    customFields: [
      { key: "user_preference", value: "dark_mode" }
    ]
  }) {
    user {
      id
      customFields {
        key
        value
      }
    }
  }
}
Comment Operations
Creating Comments
mutation CreateComment($input: CreateCommentInput!) {
  createComment(input: $input) {
    comment {
      id
      content
      date
      status
      author {
        node {
          name
          email
        }
      }
    }
  }
}
Variables:
{
  "input": {
    "commentOn": 123,           # Post database ID to comment on
    "content": "Great post!",
    "author": "John Smith",     # Required if not authenticated
    "authorEmail": "[email protected]",  # Required if not authenticated
    "authorUrl": "https://example.com"  # Optional
  }
}
[!NOTE]
- Authenticated users don’t need to provide author details
- Comments may be held for moderation based on WordPress settings
- The post must have comments open to accept new comments
Updating Comments
mutation UpdateComment($id: ID!, $content: String) {
  updateComment(input: {
    id: $id
    content: $content
  }) {
    comment {
      id
      content
      modified
    }
  }
}
Deleting Comments
mutation DeleteComment($id: ID!) {
  deleteComment(input: {
    id: $id
    forceDelete: true
  }) {
    comment {
      id
      databaseId
    }
  }
}
Moderating Comments
mutation UpdateCommentStatus($id: ID!, $status: CommentStatusEnum!) {
  updateComment(input: {
    id: $id
    status: $status
  }) {
    comment {
      id
      status
    }
  }
}
Variables for moderation:
{
  "id": "commentID",
  "status": "APPROVE"  # APPROVE, HOLD, SPAM, or TRASH
}
[!IMPORTANT] Comment moderation requires proper capabilities (
moderate_comments). Regular users can typically only:
- Create new comments (when allowed)
- Edit their own comments
- Delete their own comments
Working with Mutations
Understanding Input Types
Each mutation accepts specific input types that define what data can be provided:
# Exploring input type fields
query GetInputFields {
  __type(name: "CreatePostInput") {
    inputFields {
      name
      type {
        name
        kind
      }
      description
    }
  }
}
Input types are strictly typed:
mutation CreatePost($input: CreatePostInput!) {
  createPost(input: $input) {
    post {
      id
    }
  }
}
Variables must match the input type:
{
  "input": {
    "title": "My Post",     # String
    "status": "PUBLISH",    # PostStatusEnum
    "password": null,       # Optional String
    "commentStatus": "OPEN" # Optional CommentStatusEnum
  }
}
Handling Responses
Mutations return specific types that include:
- The modified object
- Any additional fields specific to the mutation
mutation UpdatePost($id: ID!, $title: String) {
  updatePost(input: { id: $id, title: $title }) {
    # The modified post
    post {
      id
      title
      modified
    }
    # Check if the operation was successful
    success
  }
}
Error Handling
GraphQL errors can occur at different levels:
- Syntax Errors
{
  "errors": [
    {
      "message": "Syntax Error: Expected Name, found <EOF>",
      "category": "graphql"
    }
  ]
}
- Validation Errors
{
  "errors": [
    {
      "message": "Variable \"$input\" of required type \"CreatePostInput!\" was not provided.",
      "category": "validation"
    }
  ]
}
- Authorization Errors
{
  "errors": [
    {
      "message": "Sorry, you are not allowed to create posts",
      "category": "user"
    }
  ]
}
[!TIP] Always handle errors in your application code. A successful HTTP response (200) might still contain GraphQL errors.
Optimistic Updates
When building user interfaces, you can update the UI before the mutation completes:
function updatePost({ id, title }) {
  // 1. Get current data
  const originalPost = cache.get(id);
  
  // 2. Optimistically update UI
  cache.update(id, { title });
  
  // 3. Perform mutation
  mutation({
    variables: { id, title }
  }).catch(error => {
    // 4. Revert on error
    cache.update(id, originalPost);
    showError(error);
  });
}
This provides a better user experience by:
- Showing immediate feedback
- Handling offline scenarios
- Gracefully recovering from errors
Best Practices
Input Validation
- Validate Before Sending
function validatePostInput(input) {
  const errors = {};
  
  if (!input.title?.trim()) {
    errors.title = "Title is required";
  }
  
  if (input.title?.length > 200) {
    errors.title = "Title must be less than 200 characters";
  }
  
  return Object.keys(errors).length ? errors : null;
}
- Use Proper Types
# ❌ Avoid: Using improper types for variables
mutation UpdatePost($id: ID!, $status: String) {
  updatePost(input: { id: $id, status: $status })
}
# ✅ Better: Use specific types defined in the schema
mutation UpdatePost($id: ID!, $status: PostStatusEnum!) {
  updatePost(input: { id: $id, status: $status })
}
Security Considerations
- Sanitize User Input
// ❌ Avoid: Direct user input
mutation.updatePost({ 
  content: userInput 
});
// ✅ Better: Sanitize input
mutation.updatePost({ 
  content: sanitizeHtml(userInput, allowedTags) 
});
- Limit Query Depth
# ❌ Avoid: Deep nested queries in mutations
mutation CreatePost($input: CreatePostInput!) {
  createPost(input: $input) {
    post {
      author {
        posts {
          nodes {
            author {
              posts {
                nodes {
                  # Too deep!
                }
              }
            }
          }
        }
      }
    }
  }
}
# ✅ Better: Request only what you need
mutation CreatePost($input: CreatePostInput!) {
  createPost(input: $input) {
    post {
      id
      title
      author {
        node {
          name
        }
      }
    }
  }
}
Performance Tips
- Batch Related Changes
# ❌ Avoid: Multiple separate mutations
mutation UpdatePost($id: ID!) {
  updatePost(input: { id: $id, title: "New Title" }) {
    post { id }
  }
}
mutation UpdateMeta($id: ID!) {
  updatePost(input: { id: $id, customFields: [{ key: "meta", value: "value" }] }) {
    post { id }
  }
}
# ✅ Better: Single mutation with all changes
mutation UpdatePost($id: ID!) {
  updatePost(input: {
    id: $id
    title: "New Title"
    # NOTE: This is a made up field for the sake of example
    customFields: [{ key: "meta", value: "value" }]
  }) {
    post {
      id
      title
      # NOTE: This is a made up field for the sake of example
      customFields {
        key
        value
      }
    }
  }
}
- Select Specific Fields
# ❌ Avoid: Over-fetching
mutation UpdatePost($input: UpdatePostInput!) {
  updatePost(input: $input) {
    post {
      # Don't fetch everything!
      ...AllPostFields
    }
  }
}
# ✅ Better: Request specific fields
mutation UpdatePost($input: UpdatePostInput!) {
  updatePost(input: $input) {
    post {
      id
      title
      modified
    }
  }
}
Testing Mutations
- Test Input Validation
it('validates required fields', async () => {
  const { errors } = await mutate({
    mutation: CREATE_POST,
    variables: {
      input: { /* missing required fields */ }
    }
  });
  
  expect(errors[0].message).toContain('required');
});
- Test Authorization
it('requires authentication', async () => {
  const { errors } = await mutate({
    mutation: UPDATE_POST,
    variables: {
      input: { /* ... */ }
    }
  });
  
  expect(errors[0].category).toBe('user');
});
- Test Success Cases
it('creates post with valid input', async () => {
  const { data } = await mutate({
    mutation: CREATE_POST,
    variables: {
      input: {
        title: "Test Post",
        status: "PUBLISH"
      }
    }
  });
  
  expect(data.createPost.post.title).toBe("Test Post");
});
[!TIP] Consider using a testing environment with predictable data and disabled webhooks/side effects for reliable tests.