🦌

Apollo Client で複数の GraphQL API を扱う

2022/12/03に公開

この記事は MICIN Advent Calendar 2022 の 3 日目の記事です。
前回は sugai さんの、PostgreSQL on Kubernetes を 試してみたでした。

概要

複数の GraphQL API を扱う手段としては、Schema Stitching や Apollo Federation といった、新たにサーバを設けてクライアントには単一のスキーマを公開するアプローチが挙がります。

一方、例えばクライアントで GitHub API と GitLab API を使いたいというだけであれば、新たにサーバを設けることなく対応できても良さそうです。この記事では Apollo Client を使う場合におけるこの方法の一例についてご紹介します。

流れ

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 のもとで自動生成を行うようになります。

.graphqlrc.yaml
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
user.gitHub.graphql
query GitHubUser {
  user(login: "your_name") {
    id
  }
}
user.gitLab.graphql
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 を埋める必要がなくなります。

.graphqlrc.yaml
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 というライブラリがありました。

今回の記事では 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