An Intro to Mutations in GraphQL: What They Are and How to Use Them

GraphQL mutations illustration

GraphQL has become an increasingly popular alternative to REST for building web APIs. It allows clients to request exactly the data they need in a single request. While GraphQL is often discussed in the context of querying and fetching data from a server, it also supports the ability to modify server-side data through mutations.

In this article, we‘ll take an in-depth look at mutations in GraphQL:

  • What are mutations and how do they differ from queries?
  • Why are mutations necessary?
  • How do you define mutations in your GraphQL schema?
  • How do you execute mutations from a GraphQL client?

We‘ll walk through examples of common mutations and best practices for implementing them in your GraphQL API. By the end, you‘ll have a solid understanding of mutations and how to use them effectively in your projects.

What are GraphQL Mutations?

Simply put, a mutation is an operation sent by the client that modifies data on the backend. While a GraphQL query is only used to fetch data, a mutation both retrieves data and modifies it on the server.

Some common examples of mutations include:

  • Creating a new record (e.g. a new user or blog post)
  • Updating an existing record
  • Deleting a record
  • Performing custom business logic that alters data

Just like how GET requests are used to fetch data in a REST API while POST, PUT, PATCH and DELETE requests are used to modify data, queries are used to fetch data in GraphQL while mutations are used for modifying data.

The key difference is that in REST each operation typically maps to a single endpoint, whereas in GraphQL you define your queries and mutations as fields on a single endpoint. This allows for more flexibility and efficiency, since multiple mutations can be sent in a single request.

Why Mutations are Necessary

You may be wondering – if the resolver functions for queries have access to the context and arguments, couldn‘t they be used to modify the underlying data? While technically possible, this would be a misuse of queries.

There are a few key reasons why mutations should be used for modifying data instead:

  1. Separation of concerns – queries are for fetching data, mutations are for modifying it. Keeping these concerns separate makes your API more maintainable and easier to reason about.

  2. Following established conventions – mutations align GraphQL with conventions from REST and other API paradigms where there is a clear separation between fetching and modifying.

  3. Explicit rather than implicit – a client should be aware that an operation may modify data, rather than having it happen implicitly in a query. Mutations make it explicit.

  4. Improved logging and analytics – having separate actions for reading and writing enables better logging, metrics, and analytics around how clients are interacting with your API.

Therefore, while it‘s possible to modify data in query resolvers, it‘s considered best practice to always use mutations for anything that creates, updates, or deletes data. Queries should only fetch data without side effects.

Defining Mutations in a GraphQL Schema

Mutations are defined in a GraphQL schema much like queries. The key difference is that they use the "Mutation" type instead of the "Query" type.

Here‘s an example schema that defines a simple mutation for creating a new blog post:

type Mutation {
  createPost(title: String, body: String, authorId: ID): Post
}

type Post {
  id: ID
  title: String
  body: String 
  author: User
}

type User {
  id: ID
  name: String
  posts: [Post]
}

In this schema, we define a "createPost" mutation that takes in title, body, and authorId arguments. It returns a newly created Post object.

The mutation‘s arguments are defined like function parameters, specifying their name and type. These arguments act as inputs that the client passes in when executing the mutation.

Notice how the mutation returns a Post object, allowing the client to query fields on the created post directly in the mutation. This is a useful technique for fetching updated data after a write.

Also note how the mutation is defined as a top-level field on the Mutation type, rather than the Query type. To execute this mutation, it must be sent to the root mutation field in a request.

Input Types

For more complex mutations, it‘s often useful to use input types rather than passing a long list of arguments. Input types are special object types that can be passed in as arguments.

Here‘s an example of the createPost mutation refactored to use an input type:

type Mutation {
  createPost(post: PostInput): Post
}

input PostInput {
  title: String
  body: String
  authorId: ID
}

Now, the createPost mutation takes a single "post" argument which is a PostInput type. The PostInput contains all of the fields necessary to create a new post.

Using input types makes your mutations more readable and maintainable, especially as the number of arguments grows. It also ensures naming consistency since the input type enforces a specific schema.

Mutation Resolvers

In order to actually implement the logic for a mutation, you need to provide a resolver function. The resolver contains the code that is executed when the mutation is called, similar to a controller action in a REST API.

Here‘s an example resolver for the createPost mutation:

const resolvers = {
  Mutation: {
    createPost: async (parent, args, context) => {
      const { title, body, authorId } = args.post;
      const newPost = await context.db.Post.create({
        title,
        body, 
        authorId
      });
      return newPost;
    }
  }
}

This resolver function takes in three arguments:

  1. parent – the previous object, which is not used here
  2. args – an object containing any arguments passed to the mutation (in this case the PostInput)
  3. context – a shared object available to all resolvers that typically contains things like authentication info or database connections

The resolver extracts the title, body, and authorId from the args.post input. It then creates a new post record in the database using the db.Post.create method, passing in the extracted values.

Finally, the resolver returns the newly created post object, allowing the client to query fields on it in the response.

Mutation resolvers can perform whatever logic is necessary for the given operation – creating, updating, or deleting records, performing validation, integrating with other services, etc.

The key things to keep in mind are:

  1. Mutations should always return something, typically the created/updated record or a success status, to acknowledge it was completed
  2. Authentication and authorization checks should be added to mutations to secure writes to the API
  3. Validation and error handling is important to ensure the integrity of the data and provide useful feedback to the client

Executing Mutations

Mutations are executed in a very similar way to queries from a GraphQL client. The key difference is that you use the "mutation" keyword instead of "query".

Here‘s an example of executing the createPost mutation using the popular graphql-request library:

import { request } from ‘graphql-request‘;

const mutation = `
  mutation CreatePost($post: PostInput) {
    createPost(post: $post) {
      id
      title
      body
      author {
        id
        name
      }
    }
  }
`;

const variables = {
  post: {
    title: ‘My new post‘,
    body: ‘Hello world‘,
    authorId: ‘123‘ 
  }
};

request(‘http://api.example.com/graphql‘, mutation, variables)
  .then(data => console.log(data))
  .catch(err => console.error(err));

In this code, we first define the mutation query as a string. It includes the name of the mutation, any variables it accepts ($post in this case), as well as the fields we want to query on the returned Post object.

We then define a variables object containing the data we want to pass to the mutation, in this case the title, body, and authorId of the post wrapped in a "post" key to match the name of the argument.

Finally, we execute the mutation by passing the GraphQL endpoint URL, the mutation query string, and the variables to the request function. This sends a POST request with the mutation data to the GraphQL server.

The response will come back in the "data" key of the returned object. We can log it out or inspect it to see the newly created post data that was returned by the server.

One nice thing about GraphQL mutations is that you can query fields on the returned object directly in the mutation. This allows you to get updated data back in a single request and saves you from having to send a followup query to fetch the new data.

Common Types of Mutations

We‘ve looked at an example of a "create" style mutation for adding a new record. Let‘s walk through a few other common types of mutations you‘ll see in a GraphQL API.

Update Mutation

An update mutation is used to modify an existing record. It typically takes an ID to identify the record to update as well as an input object containing the new field values.

type Mutation {
  updatePost(id: ID, post: PostInput): Post
}

input PostInput {
  title: String
  body: String
}

The resolver would then fetch the post by ID from the database, update the fields present in the input object, save it back to the database, and return the updated post object.

Delete Mutation

A delete mutation is used to delete an existing record. It usually just takes an id argument to identify the record to delete.

type Mutation {
  deletePost(id: ID): Boolean
}

The resolver would delete the post from the database by ID and return a Boolean to indicate if it was successful or not.

Custom Action Mutation

Mutations aren‘t limited to basic CRUD actions. You can define mutations for any kind of operation that modifies data.

For example, you might have a mutation that assigns a post to a different author:

type Mutation {
  assignAuthor(postId: ID, authorId: ID): Post
}

Or a mutation that publishes/unpublishes a post:

type Mutation {
  publishPost(postId: ID): Post
}

The naming and signature of the mutation should describe the action it performs. The resolver can then implement any required business logic, validation, etc. and return the modified record.

Mutation Best Practices and Considerations

When designing mutations for a GraphQL API, there are a few best practices and things to keep in mind:

  1. Use semantic naming – mutations should be named as verbs describing the action they perform, e.g. "createPost", "updateProfile", "addToCart". Be sure to follow a consistent naming convention.

  2. Embrace specificity – rather than generic mutations like "updateUser", consider more specific mutations like "changeEmail", "updateBio", etc. This makes the purpose of the mutation clear and allows for granular security controls.

  3. Provide useful responses – mutations should return whatever data the client is likely to need after executing it. Usually this is the created or updated resource as well as a success status.

  4. Use input types for complex inputs – once a mutation has more than 2-3 arguments, consider using an input type to improve readability and maintainability.

  5. Secure your mutations – add proper authentication and authorization checks in the mutation‘s resolver. Use tools like GraphQL directives for reusable auth logic across multiple mutations.

  6. Consider mutation side-effects – a mutation may have ripple effects beyond just the database write, like triggering emails, webhooks, or affecting aggregate totals. Be sure to think through and test these scenarios.

  7. Version your mutations – if you need to change a mutation in a breaking way, consider adding a new version of it instead to maintain backwards compatibility for existing clients.

By keeping these practices in mind, you can design a set of robust, maintainable mutations that fit the needs of your API and its clients.

Conclusion

Mutations are a key part of any GraphQL API, allowing clients to create, update, and delete data on the server. By providing a structured way to define and execute data modifications, they bring the benefits of GraphQL‘s strong typing and declarative nature to writing data.

In this article we covered:

  • What mutations are and how they differ from queries
  • Why mutations are necessary for modifying data
  • How to define mutations in a GraphQL schema
  • How to implement mutation resolvers
  • How to execute mutations from a GraphQL client
  • Common types of mutations and best practices

With these fundamentals in mind, you‘re well equipped to start adding mutations to your GraphQL APIs. They provide a powerful, flexible way to give your clients the ability to modify data, while maintaining the benefits of a strongly typed schema.

So next time you‘re designing a GraphQL API, be sure to think through your mutation layer with as much care as your query layer. Together, they‘ll allow your clients to fetch and modify data in an efficient, maintainable way.

Similar Posts