Apollo Client で複数の GraphQL API を扱う
この記事は MICIN Advent Calendar 2022 の 3 日目の記事です。
前回は sugai さんの、PostgreSQL on Kubernetes を 試してみたでした。
概要
複数の GraphQL API を扱う手段としては、Schema Stitching や Apollo Federation といった、新たにサーバを設けてクライアントには単一のスキーマを公開するアプローチが挙がります。
一方、例えばクライアントで GitHub API と GitLab API を使いたいというだけであれば、新たにサーバを設けることなく対応できても良さそうです。この記事では Apollo Client を使う場合におけるこの方法の一例についてご紹介します。
流れ
split
を使用してリクエスト先 URL を動的にする
Apollo Link の Apollo Client のリクエスト先 URL は Apollo Link の一種である HttpLink
を使用して指定することができます。 Apollo Link には split
なる仕組みがあり、この仕組みにより条件に応じて使用すべき Apollo Link を動的にできます。
この仕組みを用いて使用すべき HttpLink
を動的にする、すなわちリクエスト先の URL を動的にすることができるということとなります。
動的にするための条件としては、operation(query や mutation)実行時にメタ情報として設定できる context
が使用できます。
useQuery(USER, {
context: {
api: "GitHub",
},
});
new ApolloClient({
link: ApolloLink.split(
(operation) => operation.getContext().api === "GitHub",
new HttpLink({ uri: GITHUB_URI }), // 実際は認証のためのヘッダーを設定する必要がありますが割愛します
new HttpLink({ uri: GITLAB_URI }), // 同上
),
});
GraphQL Code Generator で対象の API に基づいて型等の自動生成を行う
GraphQL スキーマと operation から型や DocumentNode を生成する GraphQL Code Generator にはプロジェクトなる仕組みがあり、単一の設定ファイルで複数のスキーマを独立して扱うことができます。
例えば以下のような .graphqlrc.yaml
を用意することで、*.gitHub.graphql
で記述した operation については graphql/gitHubSchema.graphql
のもとで自動生成を行い、 *.gitLab.graphql
で記述した operation については graphql/gitLabSchema.graphql
のもとで自動生成を行うようになります。
projects:
gitHub:
schema:
- "graphql/gitHubSchema.graphql"
documents:
- "src/**/*.gitHub.graphql"
extensions:
codegen:
generates:
src/types/gitHub.gen.ts:
plugins:
- typescript
src/:
preset: near-operation-file
presetConfig:
baseTypesPath: "types/gitHub.gen.ts"
extension: .gen.ts
plugins:
- typescript-operations
- typescript-react-apollo
gitLab:
schema:
- "graphql/gitLabSchema.graphql"
documents:
- "src/**/*.gitLab.graphql"
extensions:
codegen:
generates:
src/types/gitLab.gen.ts:
plugins:
- typescript
src/:
preset: near-operation-file
presetConfig:
baseTypesPath: "types/gitLab.gen.ts"
extension: .gen.ts
plugins:
- typescript-operations
- typescript-react-apollo
query GitHubUser {
user(login: "your_name") {
id
}
}
query GitLabUser {
user(username: "your_name") {
id
}
}
生成コマンドは 2 つに分割されます。
$ yarn graphql-codegen --project gitHub
$ yarn graphql-codegen --project gitLab
なおこの時、記述した operation がそれぞれのスキーマのもとで有効であるかどうかが検証されています。例えば .gitHub.graphql
に GitLab の query が紛れ込んでいた場合にエラーとなります。
生成されたコードは以下のように使用できます。
import { useGitHubUserQuery } from "./user.gitHub.gen";
import { useGitLabUserQuery } from "./user.gitLab.gen";
const { data: gitHubData } = useGitHubUserQuery({ context: { api: "GitHub" } });
const { data: gitLabData } = useGitLabUserQuery({ context: { api: "GitLab" } });
(補足)動作を確認した GraphQL Code Generator 周辺のバージョン
@graphql-codegen/cli@2.11.3
@graphql-codegen/client-preset@1.2.0
@graphql-codegen/near-operation-file-preset@2.4.0
@graphql-codegen/typescript-react-apollo@3.3.7
VS Code 上で対象の API に基づいた補完を行う
上記は GraphQL Code Generator 指定の設定ファイルでなく標準化された GraphQL Config での記述となっており、これは VS Code の GraphQL: Language Feature Support といった拡張機能も対応しています。
そのため拡張機能を入れるだけで補完の恩恵を受けられるのですが、驚くべきことに(?)上記の projects
や各々の schema
, documents
のパスの評価も機能しており、上記の設定のもとで以下が実現できています。
-
*.gitHub.graphql
編集時には GitHub のスキーマを前提とした補完が機能する -
*.gitLab.graphql
編集時には GitLab のスキーマを前提とした補完が機能する
typescript-react-apollo
の config で context
の指定を省略する
GraphQL Code Generator のプラグインである typescript-react-apollo には defaultBaseOptions
なる設定項目があり、ここで指定したものが文字通り operation のオプションのデフォルト値として用いられます。
そのため例えば以下のようにすれば、対象の API に基づいた context
が埋まるようになり、useQuery
等の利用時に context
を埋める必要がなくなります。
projects:
gitHub:
# ..
extensions:
codegen:
generates:
# ..
src/:
# ..
config:
defaultBaseOptions:
context:
api: GitHub
gitLab:
# ..
extensions:
codegen:
generates:
# ..
src/:
# ..
config:
defaultBaseOptions:
context:
api: GitLab
import { useGitHubUserQuery } from "./user.gitHub.gen";
import { useGitLabUserQuery } from "./user.gitLab.gen";
const { data: gitHubData } = useGitHubUserQuery();
const { data: gitLabData } = useGitLabUserQuery();
operation を利用する側ではリクエスト先を意識する必要がなくなるのがめでたいですね。
その他
apollo-multi-endpoint-link
apollo-multi-endpoint-link というライブラリがありました。
今回の記事では context
を埋め、Apollo Link の split
でそれに応じて分岐する、というアプローチを採ったのに対し、apollo-multi-endpoint-link は以下のようなアプローチを採っています。
- GraphQL の operation 記述時にディレクティブを用いてリクエスト先 API の識別子を記述する
- ライブラリが提供する
MultiAPILink
により operation のディレクティブを抽出してリクエスト先を動的にする
query GitHubUser @api(name: "GitHub") {
user(login: "your_name") {
id
}
}
new MultiAPILink({
endpoints: {
GitHub: GIT_HUB_URL,
GitLab: GIT_LAB_URL,
},
createHttpLink: () => new HttpLink({ ... }),
}),
リクエスト先 API は operation の内容と密であることから、operation 利用時にコード上で context
を指定するよりも、このように operation 定義時に指定する方が良さそうに思われます。
ただ前節のように GraphQL Code Generator にその解決を任せられるのであればそれに越したことはなさそうです。
client-preset
typescript-react-apollo で以下のように記載されているように、GraphQL Code Generator は typescript-react-apollo
等でなく client-preset
を使う形を推しているようです。
We now recommend using the client-preset package for a better developer experience and smaller impact on bundle size.
client-preset
を使って複数 API をいい感じに扱えるようにする方法については気が向いた時に調査してみたいと思います。
キャッシュが干渉しうる
リクエスト先 API が別々であっても Apollo Client のキャッシュは同一であるため、同じ __typename
で同じ ID のものが仮にあれば干渉しそうです。
特にトップレベルのクエリはクエリ名がそのまま ROOT_QUERY のフィールドとして追加されるので、衝突する可能性が比較的高そうです。
キャッシュを独立して持つ or 固定で prefix を設ける等して干渉させないようにするといったうまいやり方は現状なさそうに思っています……。
おわりに
この記事では Apollo Client を使う場合における複数 API を扱う方法の一例をご紹介しました。
対象となる API に応じた operation をそのスキーマのもとで記述・検証することができ、利用側はその内情に関与せずに扱えるので、使い勝手は悪くなさそうです。
MICIN ではメンバーを大募集しています。
「とりあえず話を聞いてみたい」でも大歓迎ですので、お気軽にご応募ください!
MICIN 採用ページ:https://recruit.micin.jp/
Discussion