Designing Response Types
Best practices for query and mutation response types
Response types for mutations
This response type pattern is useful for mutations because they can result in many valid failure states (such as attempting to delete an object that doesn't exist).
For example, let's say we have an e-commerce graph. When executing a checkout
mutation, it's valid for that mutation to fail if a purchased item is out of stock or the buyer has insufficient funds.
If we define a set of ErrorResponse
types in our schema, frontend developers can provide customized experiences based on the type of failure that occurred:
1interface MutationResponse {
2 status: ResponseStatus!
3}
4
5type CheckoutSuccess implements MutationResponse {
6 status: ResponseStatus!
7 cart: [Product!]!
8 invoice: Invoice!
9}
10
11interface ErrorResponse {
12 status: ResponseStatus!
13 message: String!
14}
15
16type CheckoutError implements ErrorResponse {
17 status: ResponseStatus!
18 message: String!
19}
20
21union CheckoutResponse = CheckoutSuccess | CheckoutError
22
23type Mutation {
24 checkout(cart: ID!): CheckoutResponse!
25}
Response types for queries
Let's say we have a basic GraphQL API that defines the following Query
type:
1type Query {
2 users: [User!]!
3}
This Query.users
field makes intuitive sense: if you query it, you get back a list of User
objects. However, this return type doesn't provide any insight into the result:
If the list is empty, is that because there are zero users, or did an error occur?
Even if the list is populated, did the API return all users or just a subset?
Are there multiple pages of results?
To answer questions like these, it may be helpful for top-level fields of Query
to return "wrapper" objects that can include both the operation result and metadata about the operation's execution. For example, in cases where you need to paginate your results or implement Relay-style connections this pattern can be helpful.
Example: UsersResponse
The following example defines a UsersResponse
type for our Query.users
field:
1type User {
2 id: ID!
3 firstName: String!
4 lastName: String!
5}
6
7type UsersResponse {
8 offset: Int!
9 limit: Int!
10 totalResults: Int!
11 data: [User!]!
12}
13
14type Query {
15 users(limit: Int = 10, offset: Int = 0): UsersResponse!
16}
With this change, a client's query can specify how many results per page, what page to start on, and understand the total number of results that can be paginated:
1query FetchUsers {
2 users(limit: 20, offset: 0) {
3 totalResults
4 data {
5 id
6 firstName
7 lastName
8 }
9 }
10}