Given the following GraphQL mutations:
type Mutation {
updateUser(id: ID!, newEmail: String!): User
updatePost(id: ID!, newTitle: String!): Post
}
The Apollo docs state that it's totally possible to perform multiple mutations in one request, say
mutation($userId: ID!, $newEmail: String!, $postId: ID!, $newTitle: String!) {
updateUser(id: $userId, newEmail: $newEmail) {
id
email
}
updatePost(id: $postId, newTitle: $newTitle) {
id
title
}
}
1. Does anyone actually do this? And if you don't do this explicitly, will batching cause this kind of mutation merging?
2. If you perform run multiple things within on mutation, how would you handle errors properly?
I've seen a bunch of people recommending to throw errors on the server so that the server would respond with something that looks like this:
{
errors: [
{
statusCode: 422,
error: 'Unprocessable Entity'
path: [
'updateUser'
],
message: {
message: 'Validation failed',
fields: {
newEmail: 'The new email is not a valid email address.'
}
},
},
{
statusCode: 422,
error: 'Unprocessable Entity'
path: [
'updatePost'
],
message: {
message: 'Validation failed',
fields: {
newTitle: 'The given title is too short.'
}
},
}
],
data: {
updateUser: null,
updatePost: null,
}
}
But how do I know which error belongs to which mutation? We can't assume, that the first error in the errors
array belongs to the first mutation, because if updateUser
succeeds, the array would simple contain one entry. Would I then have to iterate over all errors and check if the path matches my mutation name? :D
Another approach is to include the error in a dedicated response type, say UpdateUserResponse
and UpdatePostResponse
. This approach enables me to correctly address errors.
type UpdateUserResponse {
error: Error
user: User
}
type UpdatePostResponse {
error: Error
post: Post
}
But I have a feeling that this will bloat my schema quite a lot.
In short, yes, if you include multiple top-level mutation fields, utilize the path
property on the errors to determine which mutation failed. Just be aware that if an error occurs deeper in your graph (on some child field instead of the root-level field), the path will reflect that field. That is, an execution error that occurs while resolving the title
field would result in a path
of updatePost.title
.
Returning errors as part of the data
is an equally valid option. There's other benefits to this approach to:
errors
array, making it part of your schema means that clients will be aware of the structure of these error objects. This is particularly important for clients written in typed languages where client code is often generated from the schema.A third alternative is to utilize unions in a similar fashion:
type Mutation {
updateUser(id: ID!, newEmail: String!): UpdateUserPayload!
}
union UpdateUserPayload = User | Error
This enables clients to use fragments and the __typename
field to distinguish between successful and failed mutations:
mutation($userId: ID!, $newEmail: String!) {
updateUser(id: $userId, newEmail: $newEmail) {
__typename
... on User {
id
email
}
... on Error {
message
code
}
}
}
You can get even create specific types for each kind of error, allowing you to omit any sort of "code" field:
union UpdateUserPayload = User | EmailExistsError | EmailInvalidError
There's no right or wrong answer here. While there are advantages to each approach, which one you take comes ultimately comes down to preference.