March 28, 2017

Designing GraphQL Mutations

Caleb Meredith

Caleb Meredith

Designing a good GraphQL API is tricky, because you always want to balance utility and convenience with a consideration around how the API may evolve in the future.

The main points to consider when designing your GraphQL mutations are:

  • Naming. Name your mutations verb first. Then the object, or “noun,” if applicable. Use camelCase.
  • Specificity. Make mutations as specific as possible. Mutations should represent semantic actions that might be taken by the user whenever possible.
  • Input object. Use a single, required, unique, input object type as an argument for easier mutation execution on the client.
  • Unique payload type. Use a unique payload type for each mutation and add the mutation’s output as a field to that payload type.
  • Nesting. Use nesting to your advantage wherever it makes sense.

This article will explain the rationale behind each of these points and equip you to design an effective GraphQL mutation system for your API.

Picking a name for your mutation

In short, try to name your mutation verb first. Your mutation represents an action so start with an action word that best describes what the mutation does.

Names like createUserlikePostupdateComment, and reloadUserFeed are preferable to names like userCreatepostLikecommentUpdate, and userFeedReload.

Some teams, like the GraphQL team at Shopify, prefer to write names the other way around — userCreate over createUser. This is useful for schemas where you want to order the mutations alphabetically (the Ruby GraphQL gem always orders fields alphabetically) and your data model is mostly object-oriented with CRUD methods (create, read, update, and delete). When all your mutations are centered around a handful of object types this convention makes sense.

However, many applications have mutations that do not map directly to actions that can be performed on objects in your data model. For instance, say you were building a password reset feature into your app. To actually send that email you may have a mutation named: sendPasswordResetEmail. This mutation is more like an RPC call then a simple CRUD action on a data type.

The password reset email case is also a good use case for illustrating why you want specific mutations over general mutations. You may be tempted to create a mutation like sendEmail(type: PASSWORD_RESET) and call this mutation with all the different email types you may have. This is not a good design because it will be much more difficult to enforce the correct input in the GraphQL type system and understand what operations are available in GraphiQL. For example, in the future you might want to send a new input argument with one of your emails. If you start with a general “catch-all” mutation, adding extra inputs is much more difficult.

Don’t be afraid of super specific mutations that correspond exactly to an update that your UI can make. Specific mutations that correspond to semantic user actions are more powerful than general mutations. This is because specific mutations are easier for a UI developer to write, they can be optimized by a backend developer, and only providing a specific subset of mutations makes it much harder for an attacker to exploit your API.

Naming conventions vary on a team-by-team basis. If you want to pick a different naming convention that’s fine, but stick to it! The above suggestion is the naming convention that I find works best for the largest range of use cases.

Designing the mutation input

Mutations should only ever have one input argument. That argument should be named input and should have a non-null unique input object type. In other words, your mutations should look like:

updatePost(input: { id: 4, newText: "..." }) { ... }

Instead of:

updatePost(id: 4, newText: "...") { ... }

But why? The reason is that the first style is much easier to use client-side. The client is only required to send one variable with per mutation instead of one for every argument on the mutation.

This seems like a small difference, but when you have mutations that need 10+ arguments your mutation GraphQL file will become much smaller.

mutation MyMutation($input: UpdatePostInput!) {
  updatePost(input: $input) { ... }
}

# vs.

mutation MyMutation($id: ID!, $newText: String, ...) {
  updatePost(id: $id, newText: $newText, ...) { ... }
}

The next thing you should do is nest the input object as much as possible. In GraphQL schema design nesting is a virtue. For no cost besides a few extra keystrokes, nesting allows you to fully embrace GraphQL’s power to be your version-less API. Nesting gives you room on your object types to explore new schema designs as time goes on. You can easily deprecate sections of the API and add new names in a conflict free space instead of fighting to find a new name on a cluttered collision-rich object type.

Think of nesting as an investment into the future of your API. See the following for an example:

mutation {
  createPerson(input: {
    # By nesting we have room at the top level of `input`
    # to add fields like `password`, or metadata fields like
    # `clientMutationId` for Relay. We could also deprecate
    # `person` in the future to use another top level field
    # like `partialPerson`.
    password: "qwerty"
    person: {
      id: 4
      name: "Budd Deey"
    }
  }) { ... }
  updatePerson(input: {
    # The `id` field represents who we want to update.
    id: 4
    # The `patch` field represents what we want to update.
    patch: {
      name: "Budd Deey"
    }
  }) { ... }
}

Designing the mutation payload

Just like when you design your input, nesting is a virtue for your GraphQL payload. Always create a custom object type for each of your mutations and then add any output you want as a field of that custom object type. This will allow you to add multiple outputs over time and metadata fields like clientMutationId or userErrors. Just like with inputs nesting is an investment that will pay off.

mutation {
  createPerson(input: { ... }) {
    # You could add other fields now to your mutation payload.
    # Like `clientMutationId` or `userErrors`.
    person {
      id
      name
    }
  }
  updatePerson(input: { ... }) {
    person {
      id
      name
    }
  }
}

Even if you only want to return a single thing from your mutation, resist the temptation to return that one type directly. It is hard to predict the future, and if you choose to return only a single type now you remove the future possibility to add other return types or metadata to the mutation. Preemptively removing design space is not something you want to do when designing a versionless GraphQL API.

Putting it all together.

Here is an example of a well-structured GraphQL mutation system for a TodoMVC application using the suggestions provided in this article.

type Todo {
  id: ID!
  text: String
  completed: Boolean
}

schema {
  # The query types are omitted so we can focus on the mutations!
  mutation: RootMutation
}

type RootMutation {
  createTodo(input: CreateTodoInput!): CreateTodoPayload
  toggleTodoCompleted(input: ToggleTodoCompletedInput!): ToggleTodoCompletedPayload
  updateTodoText(input: UpdateTodoTextInput!): UpdateTodoTextPayload
  completeAllTodos(input: CompleteAllTodosInput!): CompleteAllTodosPayload
}

# `id` is generated by the backend, and `completed` is automatically
# set to false.
input CreateTodoInput {
  # I would nest, but there is only one field: `text`. It would not
  # be hard to make `text` nullable and deprecate the `text` field,
  # however, if in the future we decide we have more fields.
  text: String!
}

type CreateTodoPayload {
  # The todo that was created. It is nullable so that if there is
  # an error then null won’t propagate past the `todo`.
  todo: Todo
}

# We only accept the `id` and the backend will determine the new
# `completed` state of the todo. This prevents edge-cases like:
# “set the todo’s completed status to true when its completed
# status is already true” in the type system!
input ToggleTodoCompletedInput {
  id: ID!
}

type ToggleTodoCompletedPayload {
  # The updated todo. Nullable for the same reason as before.
  todo: Todo
}

# This is a specific update mutation instead of a general one, so I
# don’t nest with a `patch` field like I demonstrated earlier.
# Instead I just provide one field, `newText`, which signals intent.
input UpdateTodoTextInput {
  id: ID!
  newText: String!
}

type UpdateTodoTextPayload {
  # The updated todo. Nullable for the same reason as before.
  todo: Todo
}

input CompleteAllTodosInput {
  # This mutation does not need any fields, but we have the space for
  # input anyway in case we need it in the future.
}

type CompleteAllTodosPayload {
  # All of the todos we completed.
  todos: [Todo]
  # If we decide that in the future we want to use connections we may
  # also add a `todoConnection` field.
}

Conclusion

With these design principles you should be equipped to design an effective GraphQL mutation system for your API.

This article represents how I currently think about designing mutations for GraphQL APIs as informed by my experience working on several kinds of projects. There were teams where the API was pre-designed for me, projects where I built a custom API, my work on PostGraphQL designing a general purpose GraphQL API for anyone, and conversations with Facebook and other production GraphQL users. These design principles also take heavy inspiration from the Relay Input Object Mutations Specification.

If you want a deep technical understanding of how mutations work in GraphQL, I’m working on an upcoming article titled “Understanding GraphQL Mutations”, coming soon to this blog.

If you have a different opinion on how mutations should be designed, that’s fine, and I want to hear it! Just know that it is important to always plan for the future when designing your mutations and any other part of your GraphQL API.

Written by

Caleb Meredith

Caleb Meredith

Read more by Caleb Meredith