When you are first learning about GraphQL, there’s a few basic traps you may fall into related to API design for mutations that can come back to bite you later on.
Fortunately, if you follow a few simple principles when designing your GraphQL mutations, you’ll be in great shape.
In this article, we’ll look through the Relay API spec for mutations by Facebook and walk through best practices to follow for your mutations (whether or not you use Relay on the frontend).
At first, it seems intuitive to have mutations on a resource just directly return that resource in GraphQL.
However, this leads to brittle mutations that cannot expand to add metadata to responses or other resources as your needs change.
Instead, Relay recommends that every mutation have its own unique output type with the suffix Payload
.
For example, if you have a mutation called CreateUser
, its output type would be called CreateUserPayload
.
Let’s make this clearer by looking at an example.
Let’s imagine we’re building a simple blog API.
At first, you may be tempted to make a CreatePost
mutation that looks like this:
mutation CreatePost(title: String!, body: String!): Post
This looks simple enough: our mutation just takes a title and body and directly returns a Post
object.
The problem comes later when we realize we would also like to add the current viewer object to refresh the list of posts on the viewer’s home screen as well.
Currently, there’s no way to add more fields to this output, because the output type is Post
, which is likely used in lots of other places.
By not using a unique output type for this mutation, the output of the mutation is locked to whatever the fields in Post
are.
Let’s look at this same mutation using a Relay-style output type.
Now, our mutation looks like this:
mutation CreatePost(title: String!, body: String!): CreatePostPayload! type CreatePostPayload { post: Post }
Using a Relay-style output type makes it easy to add more fields to the output.
If we want to add the current viewer, we could add it like so:
mutation CreatePost(title: String!, body: String!): CreatePostPayload! type CreatePostPayload { post: Post viewer: Viewer! }
This would allow us to send a mutation like this:
mutation { createPost(title: "My New Post", body: "New Post Body") { post { id name body } viewer { dashboardPosts { id name } } } }
Likewise, if we want to add more fields to the mutation response, there’s no problem – this mutation is future-proof!
While it’s not as essential as having a single, unique output-type for each mutation, it’s also a best practice to have a single input parameter called input
for each mutation.
For Relay, this parameter is indicated with the name of the mutation and the suffix Input
.
As your mutations get larger and larger and accept more and more parameters, it can quickly become difficult to keep client GraphQL in sync during development.
Moving a large parameter list into an input object like this is known as the Parameter Object Pattern, and can help simplify your code.
For example, for our CreatePost
mutation above, we can rewrite the input using a CreatePostInput
type like this:
mutation CreatePost(input: CreatePostInput!): CreatePostPayload! type CreatePostInput { title: String! body: String! }
This lets us use our mutation like this:
mutation createPost($input: CreatePostInput!) { createPost(input: $input) { post { ... } } }
Now, no matter how many more fields we add to the createPost
mutation, the above GraphQL is still correct.
One of the stranger parts of the Relay mutation spec is the parameter clientMutationId
, which shows up in both input and output types for mutations.
This parameter is very powerful though, since it allows your mutations to be made idempotent.
For example, imagine in our above example that a client sends a createPost
request, but they lose Internet for a moment during the request. How can the client tell if the request went through?
Should it retry again when it gets network access again? If we send another request, but the server did receive the previous request, we’ll end up with a duplicate post.
Fortunately, we can solve this with the clientMutationId
parameter.
This requires both the client and the server to be aware of this parameter. It allows our server to, for instance, cache the response for each client based on the clientMutationId
they sent.
So, if the server sees two requests with the same clientMutationId
, it will realize that the second request is a duplicate and return the cached response again without reapplying the content of the mutation.
This allows the client to get the response content it needs, without accidentally causing the mutation to run multiple times on the server.
By using this parameter, we’ve essentially made all of our mutations idempotent.
The Relay spec also requires sending the same clientMutationId
back to the client in the output of the mutation.
This is to help clients ensure the response they’re getting is for the mutation they requested in cases where there may be multiple similar mutations sent in quick succession.
While not strictly enforced by Relay, the spec recommends naming your mutations using a verb.
Mutations take an action in your API, so it makes sense to use an active word to indicate that some change will occur by calling the mutation.
For example, you should name a mutation with a verb like createPost
rather than with a noun like postCreation
.
An additional benefit of sticking with the Relay spec for mutations is that, as a common spec for GraphQL, it’s easy to implement.
Most common GraphQL libraries have support for Relay, like Graphene for Python, or graphql-js for Node/JavaScript.
These libraries make implementing the patterns described above easy, so you don’t need to worry about remembering to create separate named input and output types for each mutation or adding an optional clientMutationId
param.
It’s still up to you to make your code aware of clientMutationId
if you’d like to use it, though.
Relay is a good spec to follow for designing your GraphQL API even if you use Apollo or another GraphQL client on the frontend.
It’s officially supported by Facebook, the creator of both GraphQL and Relay.
They’re industry standards, and the design opinions Relay imposes on mutations will help to ensure your mutations are robust and future-proof as your API grows.
Happy mutating!
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic GraphQL requests to quickly understand the root cause. In addition, you can track Apollo client state and inspect GraphQL queries' key-value pairs.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowCompare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.