# GraphQL Web API ## API? 多くの開発者はレポジトリにデータを格納することを `persist` ではなく `save` で表現した方が良いと感じると思う ```typescript const userRepository = new UserRepository() await userRepository.persist({ id: 1, name: "YutaUra" }) // or await userRepository.save({ id: 1, name: "YutaUra" }) ``` API にはより細かい制御を行うのに適した低レベルな API と直感的・簡易的に操作を行うための高レベルな API に分類可能である。 低レベルな API ではより細かい制御が可能となるが、高レベルな API と比べて何を行なっているのかが分かりにくくなってしまう。 ```typescript const connection = await connectionPool.connect() const userRepository = new UserRepository() await connection.sql` INSERT INTO users VALUES(${1}, ${"YutaUra"}) ` // or await userRepository.save({ id: 1, name: "YutaUra" }) ``` そんなことは僕が説明するまでもなく、これまで発見されてきた多くの原理や原則によって示されている。例えば、 - 驚き最小の原則 - 最小権限の原則 - 開放/閉鎖原則 などなど... ## Web API? Web API でも同様のことが言えるはずです Web API では一般的に HTTP 通信を利用し、データを受け渡します。 (HTTP リクエストの擬似的なイメージ) ```yaml url: https://example.com/user method: POST header: content-type: application/json accept: application/json body: name: "YutaUra" ``` (HTTP レスポンスの擬似的なイメージ) ```yaml status: 201 header: content-type: application/json body: id: 1 name: "YutaUra" ``` 良い Web API というのは url, header, status, body 等をどのように設計するかという問題に帰着すると考えられます。 ## RESTful API RESTful API はリソース指向 Web API の実装パターンの1つであり、HTTP 通信における標準的な実装指針と言えるだろう ## Domain Model vs Data Structure リソース指向アーキテクチャにおけるリソースが指すものとは一体なんだろうか。 Domain Model とは業務システムをオブジェクト指向プログラミングに落とし込む際に、業務上の関心の対象を Domain Model として個別に表現したものだと思っている。 一方で Data Structure とは、Domain Model そのものではなく、それらを効率的に保存・取得するために正規化あるいは非正規化されたデータのことを指す。より一般的な表現をするならば、RDB における Table に相当する。 私は WebAPI で扱うべきリソースは Domain Model であるべきだと考える。(OOP と DDD がごちゃまぜに認識・解釈しており、綺麗な説明を行うことができないだろう。という保険を掛けさせていただく) ## 主張: RESTful では Domain Model と Data Structure を適切に分離することができない X(旧 Twitter) や Facebook のようなサービスを想定します。ユーザー規模などについては一旦無視してください。 そのサービスには仮に `User` と `Post` という Domain Model があるとします。 ```ts interface User { id: string name: string } interface Post { id: string authorId: string body: string } ``` ではこれらの Domain Model を RESTful API にマッピングしましょう。 ```yaml paths: /users: get: ... post: ... /users/{userId}: get: ... patch: ... /users/{userId}/posts: get: ... /posts: get: ... post: ... /posts/{postId}: get: ... patch: ... ``` 細かい部分は置いておいて、概ねこのような感じになると思います。 --- このサービスの仕様が変わり Domain Model としていくつかフィールドを追加することになりました。変更後の Domain Model は次のとおりです。 ```ts interface User { id: string name: string // 以下追加 followingUserCount: number followedUserCount: number totalLikeCount: number } interface Post { id: string authorId: string body: string // 以下追加 likeCount: number repostCount: number bookmarkedCount: number viewCount: number } ``` さて、RESTful な API ではこれらの追加されたフィールドをどのように表現しますか? ### パターン1 既存のエンドポイントから返すようにする RESTful API で扱うべきリソースは Domain Model なのですから、当然ですね! ```shell $ curl -v -X GET "http://example.com/api/users/12345" -H "Accept: application/json" > GET /api/users/12345 HTTP/1.1 > < HTTP/1.1 200 OK { "id": "12345", "name": "ユーザー名", "followingUserCount": 100, "followedUserCount": 50, "totalLikeCount": 250 } ``` めでたし、めでたし --- FE「なんか最近 API のレスポンス速度が大幅に低下してるんだよなぁ....」 ### パターン2 エンドポイントを分けることにする 今の `User` テーブルには今回追加する情報はないし、それを API レスポンスに含めようとするとパフォーマンス悪くなりそうだからエンドポイントを分けるのが一番良い! ```shell $ curl -v -X GET "http://example.com/api/users/12345" -H "Accept: application/json" > GET /api/users/12345 HTTP/1.1 > < HTTP/1.1 200 OK { "id": "12345", "name": "ユーザー名", } $ curl -v -X GET "http://example.com/api/users/12345/followingUserCount" -H "Accept: application/json" > GET /api/users/12345 HTTP/1.1 > < HTTP/1.1 200 OK 100 $ curl -v -X GET "http://example.com/api/users/12345/followedUserCount" -H "Accept: application/json" > GET /api/users/12345 HTTP/1.1 > < HTTP/1.1 200 OK 50 $ curl -v -X GET "http://example.com/api/users/12345/totalLikeCount" -H "Accept: application/json" > GET /api/users/12345 HTTP/1.1 > < HTTP/1.1 200 OK 250 ``` めでたし、めでたし --- BE「あれ、 RESTful API で扱うリソースって Domain Model じゃなくてもいいんだっけ...?まぁいっか!」 FE「なんで User の情報を取得するのに何度も API を叩かないといけないんだ...」 ### いろいろ指摘したい箇所はあると思います - いやいや、非正規化すればいいだけでしょ! - Domain Model としての問題と Data Structure としての問題が混ざっている時点でイマイチ - そこまで細かく分けないです! - でも少なからず分けているんでしょう - 僕たちのチームはこんな運用でカバーしています! - 運用でカバーすることが難しいとは思っていません - include, exclude, select 等のパラメーターを使います! - ref: https://learn.microsoft.com/ja-jp/graph/query-parameters?tabs=http#select-parameter - ダメではないですが、 OpenAPI Schema とかどうしてるんですかね これは完全に私の個人的な意見ですが、 Web API として Domain Model を扱うということに関して RESTful というのはあまり相性が良くないと思っています。これは私の好き嫌いや美徳だと思っています。 ## ということで GraphQL 先程のケースにおいて GraphQL を使っていたらどうだったのでしょうか。 ```graphql type User { id: ID! name: String! followingUserCount: Int! followedUserCount: Int! totalLikeCount: Int! } type Query { user(id: ID!): User } ``` GraphQL Schema では追加のフィールドも全て `User` モデルに含ませることができます。 GraphQL ではカスタムリゾルバーを割り当てることで、クライアントがフィールドを要求した時にのみ計算が行われるため、無駄な計算を省くことができます。というのをバックエンドで制御します。 ```graphql query PostDetail($id: ID!) { post(id: $id) { id body author { id name } } } query UserDetail($id: ID!) { user(id: $id) { id name followingUserCount followedUserCount } } ``` 仮に `followingUserCount` と `followedUserCount` にカスタムリゾルバーを実装割り当てしている場合に、 PostDetail ではそれらは実行されず、UserDetail でのみ実行されることになります。 また、どの場合にカスタムリゾルバーが実行されるかということに関して、クライアントは知る必要がなく、 GraphQL Engine がバックエンド側で隠蔽してくれています。 これが僕がバックエンド視点で GraphQL を使うべき理由だと思っています。GraphQL によって提供される Web API は周辺のエコシステムも含めではありますが、Web API として考えるべき関心を限定してくれる優れた仕組みだと考えます。 ## とはいえ React Server Component 細かい話はすっ飛ばして ### 主張: Node を使おう Relay では GraphQL で扱う Domain Model に対して Node と呼ばれる interface を継承させることを規定してます。(https://relay.dev/docs/guides/graphql-server-specification/#object-identification) 適切な実装を行うことで以下のようなことが可能となります。 ```graphql query PostDetail($postId: ID!, $someUserId: ID!) { node(id: $postId) { id ... on Post { body } } node(id: $someUserId) { id ... on User { name } } } ``` 何が言いたいかというと、必ずしも Fragment Collocation された巨大な GraphQL Document を解決することだけが GraphQL ではなく、 RSC のように細かいデータ取得が必要な場合でだって GraphQL を使ってもいいんです。 その場合に Relay の Node という仕組みは便利かもしれません。 もしあなたが GraphQL Engine のオーバーヘッドを認められないなら、無理に勧めることはしませんが、私は多くのケースで GraphQL Engine のオーバーヘッドは認めることができるのではと考えています。 ### もしくは `@defer`, `@stream` が使えるようになると気持ちいいと思う (あんま対応状況追えていない) ## 一方、HATEOAS とは言いつつ、個人的には HATEOAS の思想はめっちゃ好きだと思っている。 ```shell $ curl -v -X GET "http://example.com/api/users/12345" -H "Accept: application/json" > GET /api/users/12345 HTTP/1.1 > < HTTP/1.1 200 OK { "id": "12345", "name": "ユーザー名", "followingUserCount": "http://example.com/api/users/12345/followingUserCount", "followedUserCount": "http://example.com/api/users/12345/followedUserCount", "totalLikeCount": "http://example.com/api/users/12345/totalLikeCount" } ``` あくまで API 提供側で制御することができる点で、HATEOAS であれば Domain Model としての表現は十分満たしていると考えられる。 データ取得が並列ではなく、直列になってしまう点はややイマイチだなと感じる。 また、クライアントとしてはいい感じに欲しいデータを自動で解決する仕組みがあるとよいのかな〜と感じる。(GraphQL Engine のフロントエンド版みたいなイメージ?) https://recrits.hatenablog.com/entry/2022/07/20/000045 が面白かった