Production Ready GraphQLまとめ

[Production Ready GraphQL](https://book.productionreadygraphql.com/)という洋書の4章の「GraphQL Schema Design」を自分なりにまとめたもの。

## Design First

- お気に入りのライブラリでいきなり実装を始めるのではなく、設計から始める
- 実装から始めると、実装の詳細と密結合した設計になりがち
- 担当ドメインの専門家の話を聞きながら、APIを設計できるのがベスト
- 設計から始めることで破壊的な変更の頻度を減らせる

## Client First

- 設計をする際は、Clientのユースケースを念頭に置く
- Clientのニーズの要求を満たし、簡単にAPIを使えるようにし、間違った使い方はなるべくできないようにする
- 開発のなるべく早い段階からClientと結合して、フィードバックをもらうこと
- バックエンドの実装の詳細などのClientが知る必要のない情報は公開しない
- データベースからGraphQL APIを生成するツールはたくさんあるが、Client Firstの観点からはあまり意味はない
    - 生成されたスキームは、バックエンドの実装と密に結合している
    - 一般的すぎるAPIが生成されがち
    - Clientの要望を全く意識していない
    - 不要な情報を公開しすぎる
- Open APIをGraphQLスキームに変換するツールも良くはない

## Naming

- 良い命名をしていると、ドキュメントを見なくても動作が分かる
- 良い命名は正しい設計につながる
- 一貫性が一番大切
    - 悪い例:Queryに動詞が付いていたりいなかったりする
        - products
        - findProducts
    - 悪い例:表記ゆれ
        - BlogPost
        - Post
    - 悪い例:Mutationの動詞の揺れ[^1]
        - addProduct
        - createPost
- APIに対称性をもたせる
    - publishPostときたら、反対の操作はunpublishPostとする
- 命名は具体的にする
    - 具体的な命名は、大規模なAPI廃止を避けられるし、使い方も理解しやすい
    - 一般的すぎる命名の例："Event"、"User"

## Descriptions

- APIの使い手にとって、Descriptionは外部のドキュメントより見つけやすくて良い
- ほとんどのエンティティにDescriptionを書くと良い
- 良いDescriptionは、typeが何を表現するか、mutationは何をするかを明瞭にする
- とはいえ、Descriptionをあまり読まなくても、APIを理解して使えるようにすべき
- Descriptionに、エッジケースや独特な振る舞いをする条件が記述されていると良くない兆候[^2]

## Use the Schema, Luke!

- enumを使う
- stringやcustom encoding scalarにJSON文字列のような半構造化データを持たせるのではなく、typeを定義する
- custom scalarは役に立つかも (DDDの値型のような立ち位置)

## Expressive Schema

- フィールドは一つの仕事をこなすようにし、一般的だったり利口すぎるフィールドは避ける[^3]
- なるべくスキーマで正しい使い方を強制する
- 関連したフィールドや引数の集まりを表現するために、型を定義する
- optional inputsや引数を使うときは、デフォルト値を定義する

以下、具体例。

- 例1
    - いまいち
        - ```findProduct(id: ID, name: String): Product```
        - IDとnameを両方とも指定していないときの動作が読み取れない
    - 良い
        - ```productByID(id: ID!): Product```
        - ```productByName(name: String!): Product```
- 例2
    - いまいち
        - ```products(sort: SortOrder): [Product!]!```
        - SortOrderを指定しないときの並びが読み取れない
    - 良い
        - ```products(sort: SortOrder = DESC): [Product!]!```


## Specific or Generic

- おおむね特化したAPIの方が良い
    - いまいち
        - ```posts(first: Int!, includeArchived: Boolean): [Post!]!```
    - 良い
        - ```posts(first: Int!): [Post!]!```
        - ```archivedPosts(first: Int!): [Post!]!```
- booleanの引数は、汎化されすぎている兆候かもしれない
- 汎化したAPIは便利なこともあるが、使い方が分かりづらい、性能に難がある、といった問題もある[^4]

## Anemic GraphQL

- 他のフィールドから計算される値はフィールドとして定義する
    - Clientに計算させるのではなくServerで計算させるということ
    - ルールが変わるとClientを修正しないといけないから
- Mutationで万能Update API + XXXInputを定義するのではなく、ユースケース別にAPIやInputを定義する
    - optionalなフィールドがなくなる
    - APIの動作が分かりやすくなる
    - Inputに不正な値の組み合わせを設定できなくなる

## List&Pagination

### Pagination

**Offset Paginationをサポートする必要がないなら、Cursor Paginationが良い。**

- Offset Pagination
    - Good: 実装が楽、ユーザがページの飛ばし読みができる
    - Bad: 走査するデータ数が増えるので性能に難あり、リストの閲覧中に項目数が変わると整合性が取れなくなる
- Cursor Pagination
    - Offset Paginationと特徴の裏返し
    - ほとんどのGraphQL APIはこちらの方式

### Relay Connection

以下のようなスキーマ設計のこと。

```graphql
type Query {
  products(limit: Int!, after: String): ProductConnection!
}

type ProductConnection {
  edges: [ProductEdge]
  pageInfo: PageInfo!
  """
  これ以外のフィールドを持たせるのもあり。
  たとえば、node一覧へのショートカット。
  nodes: [Product!]!
  """
}

type ProductEdge {
  cursor: String!
  node: Product!
}

type PageInfo {
  endCursor: String
}

"""リスト対象"""
type Product {
  name: String!
}
```

EdgeやConnectionに情報を足せるので、色々なユースケースにも対応しやすい。

## Sharing Types

- 型を使い回すことに自信がないなら、使い回さない方が良い
- 型の使い回しは便利なときもあるが、後々、問題を起こすこともある

## Global Identification

- 慣例
    - Node interfaceを作り、それを実装する型にはグローバルに一意なIDを設定する
    - ```node(id: ID!) Node```や```nodes(ids: [ID!]!) [Node!]!```で取得できるようにする
    - IDには、型を判別するための情報も含める
- API ClientとしてRelayをサポートしないなら慣例に従うのは必須ではないが、Relayを使わなくても良い設計パターンになりえる
- IDはOpaque　にした方が良い[^5]
    - API ResponseのIDをBase64でエンコードにしておくと、使い手にOpaqueな値ということを思い出させるのに良い

## Nullability

- non-nullのメリット
    - Schemaから読み取れる情報が多くなる
    - API Client側の過度な防御コードを減らせる
- 注意
    - non-nullからnullableの変更は破壊的変更
    - どのフィールドがnullになるのか否かの予測は難しい
        - タイムアウト、API Rate Limit起因でフィールドがnullになることも
    - nullableな値をnullで返したときの挙動が独特
        - ルールに違反したフィールドの先祖をさかのぼって、最初に見つけたnullableなフィールドの値がnullになる
- ガイドライン
    - 引数
        - non-nullにすると良いことが多い
        - 既存のQuery/Mutationに引数を追加するときは互換性を保つためにnullableにするのが良い
    - Objectフィールド
        - Database、networks callのように失敗する可能性があるものから取得しているなら、nullableにした方が良い
    - 単純なスカラ値のフィールド
        - 大体、non-nullで大丈夫

## Abstract Types

- interfaceは共通の振る舞いを表現するのに使うこと
    - 例： GitHubのGraphQL APIのStarable
- interfaceの濫用はしないこと
    - 単に共通のフィールドを持っているからという理由で、interfaceを定義するのはいまいち
    - 実装時にコードを再利用したいからといってinterfaceを定義するのはいまいち

## Designing for Static Queries

- GraphQLのQueryは動的に組み立てるのではなく、静的に記述するのが良い(Queryに渡す引数を変えるのはありらしい)
- 静的Queryの利点
    - Clientが必要としていることが分かりやすい
    - Lint、IDEなどのツールの恩恵を受けられる
    - サーバがQueryを保存できる
- IDのリストからオブジェクトを一括取得するときに動的Queryを使いたくなるかもしれないが、それはBatch Read用のQueryを用意することで避けられる

## Mutations

- mutationごとに固有のPayload型を定義し、それを戻り値にする
    - 付随的な情報を返せるのがメリット
- mutationごとに固有のInputを1つ定義し、それを引数にする
    - mutation時に渡すパラメータを増やしやすい

以下、具体的イメージ。

```graphql
input PostMessageInput {
  roomID: String!
  text: String!
}

type PostMessagePayload {
  message: Message
  errors: [UserError!]!
}

type Mutation {
  postMessage(input: PostMessageInput!): PostMessagePayload
}
```

## Fine-Grained or Coarse-Grained(粒度)

- mutationについて、どれくらいの粒度が良いかは状況次第
- 作成mutationの粒度は荒く、更新mutationの粒度は細かい方が良いことが多い
- 複数のデータの更新が必要 かつ 整合性が求められる場合は、1つの粒度の粗い更新mutationを作る
    - 別解として、1つのmutationのInputのフィールドに、実行したい操作のリストを持たせるやり方もある

## Errors

- Developer/Clientエラーは、GraphQLの標準のerrorsで表現すると良い
    - Timeout、Rate Limitedなど
- Userエラーは、独自の表現が良い
    - サインアップ画面のパスワードの文字種が間違っている、2重支払いをしたなど
- mutationのErrorを独自に表現する方法には2つある
    1. カスタムエラー型を定義し、Payload型のフィールドにその配列を持たせる
    1. mutationの結果となるPayload型をunionとして定義する
        ```graphql
        union SignUpPayload = SignUpSuccess | UserNameTaken | PasswordTooWeak
        ```
        どういうエラーが起きるか分かりやすい
- エラー型を定義するときは、interfaceを定義しそれを実装する形にすると良い
    - エラー型を追加したとき、古いAPI Clientでもハンドリングできる
- 上記2つのエラーの定義方法のどちらを選択するかは好み

## Schema Organization

- Namespaces
    - 適切な名付けをしていればnamespaceは大体いらない
    - namespaceのようなものが欲しいのであれば、Prefixを付ける
        - 例：Instagram_User、Fracebook_User
    - GraphQLスキーマとサーバ側の実装は別物
        - サーバ側の実装としては、namespace、モジュール、共通関数を使うことは当然あり
- Mutations
    - グループを表す言葉を先頭にするのではなく、読みやすい命名にする
        - いまいち：productCreate、productDelete
        - Good:createProduct、deleteProduct
    - directiveの"tags"のようなものを作り、それでmutationのgroupを表現する考えもある

## Asynchronous Behavior

- 非同期の処理中であることを表すなら、処理中であることを表現する型なりenumなりを定義する
- [Shopifyの非同期処理の状態の表現方法は参考になる](https://shopify.dev/api/admin-graphql/2022-07/objects/Job)
- [Shopifyの非同期処理(Bulk Operation)の定義方法も参考になる](https://shopify.dev/api/usage/bulk-operations/queries)

## Data-Driven Schema vs Use-Case-Driven Schema

- Use-Case-Driven Schemaの方が良い
- GitHubのGraphQL APIのスキーマは、Use-Case-Drivenの良い例


[^1]: 動詞の揺れは悪くないと思っている。一貫性をもたせようとすると新規登録は全部createになりそうだが、createは一般的すぎる。チャットのAPIを作るとして、チャットルームを作るAPIはCreateChatで、メッセージ投稿APIはPostMessageというような命名の方が分かりやすい。メッセージ投稿をCreateMessageにすると、メッセージ送信なのかメッセージの下書きを作るのか分かりづらい。
[^2]: "Aの場合はαという動作になる、Bの場合はβという動作になる、Cの場合はγになる、・・・"というようにエッジケースがつらつら書かれている場合の話だと思う。一覧取得APIがあって、limitで"巨大な値を設定しても1000に設定する"というようなDescriptionなら有りだと思う。
[^3]:フィールドを安直にnullableにしてしまうと、一般的でお利口なスキーマになりがちな印象。本で示されている悪い例はnullableなスカラー型のフィールドが多かった。
[^4]:APIの使い手が多いときは汎化もありだと思う。
[^5]:IDの値の形式をAPI Clientから意識させないようにするということ。