Skip to content

Commit fc187b5

Browse files
committed
schema-design: Address mutation and mutation response feedback.
1 parent 06550cd commit fc187b5

1 file changed

Lines changed: 75 additions & 34 deletions

File tree

docs/source/guides/schema-design.md

Lines changed: 75 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ query GetBooks {
140140

141141
The `Mutation` type is a core type in GraphQL which specializes in _modifying_ data, which contrasts the `Query` type used for _fetching_ data.
142142

143-
Unlike REST, where the behavior is more ad-hoc, the `Mutation` type is designed with the expectation that there will be a response object. This ensures that the client receives the most current data without a subsequent round-trip re-query.
143+
Unlike REST, where the behavior can be more ad-hoc, the `Mutation` type is designed with the expectation that there will be a response object. This ensures that the client receives the most current data without a subsequent round-trip re-query.
144144

145145
A mutation for updating the age of a `User` might look like this:
146146

@@ -182,44 +182,21 @@ Once executed by the server, the response returned to the client might be:
182182
}
183183
```
184184

185-
The first thing to note is that it’s most common to return the thing you’re updating from a mutation. In our example, we were updating a `User` record, so we returned that updated user. There’s nothing _enforcing_ this practice, but it’s highly recommended, because it’s often needed to have an updated user to let the clients update any local cache with a new instance of that `User`. For this same reason, it’s useful design mutations to update only one entity. If you wanted to update two unrelated entities, it’s recommended to in separate mutations. For more details on how to handle errors and warnings in mutations, see the [mutation responses](#mutation-responses) section below.
185+
While it's not mandatory to return the object which has been updated, the inclusion of the updated information allows the client to confidently update its local state without performing additional requests.
186186

187-
But what if we wanted to update more than just a single or couple attributes on a user? Passing each thing we need as a single argument would get tedious. Especially if multiple mutations used similar fields. For this, we can use input types, which are explained in the next section.
187+
As with queries, it's best to design mutations with the client in mind and in response to a user's action. In simple cases, this might only result in changes to a single document, however in many cases there will be updates to multiple documents in different collections, for example, a `likePost` mutation might update the total likes for a user as well as their post.
188188

189-
<h3 id="mutation-input-types">Input types</h3>
190-
191-
Input types are a special type in GraphQL which are defined as arguments to queries and, more commonly, mutations. They can be thought of as object types for arguments, in addition to the other scalar types. Input types are especially useful when multiple mutations require similar information; for example, when creating a user and updating a user require the same fields, like `age` and `name`.
189+
In order to provide a consistent shape of response data, we recommend adopting a pattern which returns a standardized response format which supports returning any number of documents from each collection which was modified. We'll outline a recommended patterns for this in the next section.
192190

193-
Input types are used like any other type and defining them is similar to a typical object type definitions, but with the `input` keyword rather than `type`.
194-
195-
Here is an example of two mutations that operate on a `User`, _without_ using input types:
196-
197-
```
198-
type Mutation {
199-
createUser(name: String, age: Int, address: String, phone: String): User
200-
updateUser(id: ID!, name: String, age: Int, address: String, phone: String): User
201-
}
202-
```
203-
204-
To avoid the repetition of argument fields, this can be refactored to use an input type, as follows:
191+
<h3 id="mutation-responses">Responses</h3>
205192

206-
```
207-
type Mutation {
208-
createUser(user: UserInput): User
209-
updateUser(id: ID!, user: UserInput): User
210-
}
193+
GraphQL mutations can return any information the developer wishes, but designing mutation responses in a consistent and robust structure makes them more approachable by humans and less complicated to traverse in client code. There are two guiding principles which we have combined into our suggested mutation response structure.
211194

212-
input UserInput {
213-
name: String
214-
age: Int
215-
address: String
216-
phone: String
217-
}
218-
```
195+
First, while mutations might only modify a single collection, they often need to touch multiple collections. It makes sense for this to happen in a single round-trip to the server and this is one of the strengths of GraphQL! When multiple collections are modified, the client code can benefit from having updated fields returned from each collection and the response format should support that.
219196

220-
<h3 id="mutation-responses">Responses</h3>
197+
Secondly, mutations have a higher chance of causing errors than queries since they are modifying data. If only a portion of a mutation update succeeds, whether that is a partial update to a single document's fields or a failed update to an entire document, it's important to convey that information to the client to avoid stale local state on the client.
221198

222-
Mutations have a higher chance of causing errors than queries since they are modifying data. A common way to handle errors during a mutation is to simply `throw` an error. While that's fine, throwing an error in the resolver will return an error to the caller and prevent a partial response, which could be useful in the event of a partial update. Consider the following mutation example, which tries to update a user's `name` and `age`:
199+
A common way to handle errors during a mutation is to simply `throw` an error. While that's fine, throwing an error in a resolver will return an error for the entire operation to the caller and prevent a more meaningful response. Consider the following mutation example, which tries to update a user's `name` and `age`:
223200

224201
```graphql
225202
mutation updateUser {
@@ -232,7 +209,7 @@ mutation updateUser {
232209

233210
With validation in place, this mutation might cause an error since the `age` is a negative value. While it’s possible that the entire operation should be stopped, there’s an opportunity to partially update the user’s record with the new `name` and return the updated record with the `age` left untouched.
234211

235-
Luckily, the powerful structure of GraphQL mutations accommodates this use case and can return transactional information about the update alongside the records which have been changed which enables client-side updates to occur automatically.
212+
Fortunately, the powerful structure of GraphQL mutations accommodates this use case and can return transactional information about the update alongside the records which have been changed which enables client-side updates to occur automatically.
236213

237214
In order to provide consistency across a schema, we suggest introducing a `MutationResponse` interface which can be implemented on every mutation response in a schema and enables transactional information to be returned in addition to the normal mutation response object.
238215

@@ -283,7 +260,71 @@ Let’s break this down, field by field:
283260
* `message` is a string that is meant to be a human-readable description of the status of the transaction. It is intended to be used in the UI of the product.
284261
* `user` is added by the implementing type `UpdateUserMutationResponse` to return back the newly created user for the client to use!
285262

286-
Following this pattern for mutations provides detailed information about the data that has changed and feedback on whether the operation was successful or not. Armed with this information, developers can easily react to failures in the client and fetch the information they need to update their local cache.
263+
For mutations which have touched multiple collections, this same structure can be used to return updated objects from each collection. For example, a `likePost` type, which could affect a user's "reputation" and also update the post itself, might implement `MutationResponse` in the following manner:
264+
265+
```graphql
266+
type LikePostMutationResponse implements MutationResponse {
267+
code: String!
268+
success: Boolean!
269+
message: String!
270+
post: Post
271+
user: User
272+
}
273+
```
274+
275+
In this response type, we've provided the expectation that both the `user` and the `post` would be returned and an actual response to a `likePost` mutation could be:
276+
277+
```json
278+
{
279+
"data": {
280+
"likePost": {
281+
"code": "200",
282+
"success": true,
283+
"message": "Thanks!",
284+
"post": {
285+
"likes": 5040
286+
},
287+
"user": {
288+
"reputation": 11
289+
}
290+
}
291+
}
292+
}
293+
```
294+
295+
Following this pattern for mutations provides detailed information about the data that has changed and feedback on whether the operation was successful or not. Armed with this information, developers can easily react to failures within the client
296+
297+
<h3 id="mutation-input-types">Input types</h3>
298+
299+
Input types are a special type in GraphQL which are defined as arguments to queries and, more commonly, mutations. They can be thought of as object types for arguments, in addition to the other scalar types. Input types are especially useful when multiple mutations require similar information; for example, when creating a user and updating a user require the same fields, like `age` and `name`.
300+
301+
Input types are used like any other type and defining them is similar to a typical object type definitions, but with the `input` keyword rather than `type`.
302+
303+
Here is an example of two mutations that operate on a `User`, _without_ using input types:
304+
305+
```
306+
type Mutation {
307+
createUser(name: String, age: Int, address: String, phone: String): User
308+
updateUser(id: ID!, name: String, age: Int, address: String, phone: String): User
309+
}
310+
```
311+
312+
To avoid the repetition of argument fields, this can be refactored to use an input type, as follows:
313+
314+
```
315+
type Mutation {
316+
createUser(user: UserInput): User
317+
updateUser(id: ID!, user: UserInput): User
318+
}
319+
320+
input UserInput {
321+
name: String
322+
age: Int
323+
address: String
324+
phone: String
325+
}
326+
```
327+
287328

288329
<h2 id="gql">Wrapping documents with `gql`</h2>
289330

0 commit comments

Comments
 (0)