Design predictable GraphQL API
Tags: graphql, api
Published: 2021-08-15 13:54:58

Today I am going to tell you how we design our GraphQL API and how we make it predictable. 

Unfortunately, knowledge of GraphQL doesn’t answer the question how to design API. And I would be happy to find such an article when we started. Every time we faced problems, we had to make decisions on the go. Some of them took root, while others were altered as a result of retrospective. Finally, we made a number of strict rules that have been helping us structure our API without freedom of interpretation for a long time. I have no illusion that these rules will work for every project. But I'm sure they can help many of us find answers when we need them.

## Queries

Let's take a look at the book accounting service API for a some Library as an example. We will operate with such entities as author, book, collection and user. 

### Rule #1: provide a resolver to get entity data named according to entity name

In our example, we will declare 4 resolvers to get the data of the corresponding entities

```graphql
type Query {
  author(id: ID!): Author 
  book(id: ID!): Book
  collection(id: ID!): Collection
  user(id: ID!): User
}
```

In relationships between entities, this rule also works

```graphql
type Book {
  id: ID!
  title: String!
  author: Author!
}
```

### Rule #2: provide a resolver to get a list of entity data named according to the entity's plural name*

There are several nuances here. Data lists can be either limited or not. For small limited lists, you can simply return an array of items

```graphql
type Collection {
  id: ID!
  title: String!
  books: [Book!]!
}
```

But of course, we cannot do this with either authors, books, or users, because these lists have no limits.

### Rule #3: unlimited lists must be paginated

There are two common types of pagination: offset-based and [cursor-based](https://relay.dev/graphql/connections.htm). The truth is, you will most likely want to use both for different tasks. So, for the offset-based paginated list we leave the entity name in the plural and for cursor-based paginated list we add the `Connection` suffix (*).

```graphql
type Query {
  authors(offset: Int, limit: Int): Authors
  books(offset: Int, limit: Int): Books
  booksConnection(
    before: String
    after: String
    first: Int
    last: Int
  ): BooksConnection
  collectionsConnection(
    before: String
    after: String
    first: Int
    last: Int
  ): CollectionsConnection
  users(offset: Int, limit: Int): Users
}
```

As you can see, the object types for paginated lists are named according to the resolvers.

### Rule #4: resolvers must be universal regardless of the client's access level

You should not create different resolvers to receive data from the same entity, depending on the access level. There are other ways to differentiate access to fields such as [schema directives](https://www.graphql-tools.com/docs/schema-directives#enforcing-access-permissions). 

```graphql
# 😡 Bad way
type Query {
  user(id: ID!): User
  userByAdmin(id: ID!): UserByAdmin
}

type User {
  id: ID!
  login: String!
}

type UserByAdmin {
  id: ID!
  login: String!
  email: String!
}
```

```graphql
# 😊 Good way
directive @auth(requires: Role = ADMIN) on OBJECT | FIELD_DEFINITION

enum Role {
  ADMIN
  GUEST
}

type Query {
  user(id: ID!): User
}

type User {
  id: ID!
  login: String!
  email: String! @auth(requires: ADMIN)
}
```

### Rule #5: add resolvers as fields to entity types to calculate their characteristics instead of separate resolvers

In GraphQL, any field of an object type can return not just the data stored in it, but be processed by a separate resolver. Due to this, the entity dataset can be enriched with additional, calculated parameters.

```graphql
# 😡 Bad way
type Query {
  book(id: ID!): Book
  isBookBorrowed(id: ID!): Boolean
}
```

```graphql
# 😊 Good way
type Query {
  book(id: ID!): Book
}

type Book {
  id: ID!
  title: String!
  author: Author!

  isBorrowed: Boolean!
}
```

### Rule #6: if requested data doesn't exist, you must return `null` or empty list `[]` instead of an exception

The main point of this rule: you should not throw an exception in query resolvers. Lack of data is not a error.

You may have noticed in the query resolvers examples above,  there is no Non-Null modifier (`!`) for the return data type. It means that these resolvers can return `null` instead of object types.

In cases where a list is supposed, you must return an empty array `[]`. You should always use the Non-Null modifier in conjunction with the list modifier `[Entity!]!`. You don't need other combinations `[Entity]`, `[Entity!]` or `[Entity]!` which will allow you to pass `null` instead of a list or as an item.

```graphql
type Query {
  collection(id: ID!): Collection
}

type Collection {
  id: ID!
  title: String!
  books: [Book!]!
}
```

## Mutations

### Rule #7: CRUD mutations of an entity must be named according to the pattern `${operation}${Entity}`

```graphql
type Mutation {
  createUser(userData: CreateUserInput!): User!
  updateUser(userData: UpdateUserInput!): User!
  deleteUser(id: ID!): User!
}
```

### Rule #8: CRUD mutations of an entity must return entity data

As in the example above, even a delete mutation returns deleted entity data.

### Rule #9: it is necessary to distinguish between arguments that are heterogeneous data and the input data of an entity

Parameters of single entity must be combined into input object type and passed in one argument.

```graphql
type Mutation {
  createUser(userData: CreateUserInput!): User!
}

input CreateUserInput {
  login: String!
  email: String!
}
```

Dissimilar arguments must be passed separately.

```graphql
type Mutation {
  borrowBook(userId: ID!, bookId: ID!): Borrowing!
}
```

### Rule #10: the name of the input type must match the pattern `${Mutation}Input`

```graphql
type Mutation {
  createUser(userData: CreateUserInput!): User!
  updateUser(userData: UpdateUserInput!): User!
}
```

### Rule #11: the name of the argument corresponding to the entity's input type must match the pattern `${entity}Data`.

```graphql
type Mutation {
  createUser(userData: CreateUserInput!): User!
  updateUser(userData: UpdateUserInput!): User!
}
```

### Rule 12: all fields of the input type of the update mutation except the identifying ones must be optional

```graphql
type Mutation {
  updateUser(userData: UpdateUserInput!): User!
}

input UpdateUserInput {
  id: ID!
  login: String
  email: String
}
```

This rule allows you to use a single mutation to update different parts of the entity separately.

```graphql
mutation UpdateUserEmail {
  updateUser(
    userData: {
      id: "User ID"
      email: "New e-mail"
    }
  ) {
    id
    email
  }
}
```

### Rule 13: if for some reason the operation cannot be performed, an exception must be thrown

In this regard, mutations are different from queries.

## Conclusion

The subscriptions are remaining overboard for now. We continue to gain experience with them and maybe later I will share the results with you.

I would love to discuss these 13 rules with you (don't let the number scare you). Please share your experience on how you approach API design in your projects. Tell me in what cases these rules will not work and how you get out of the situation.

Thank you for your attention!