GraphQLエンドポイントが複数ある時のフロントエンド
最近GraphQLについて触る機会があったのでその過程で調査したことについてのメモです。
GraphQLエンドポイントが複数あり、それぞれに個別のSchemaが定義されている場合、以下の点についてまとめています
- そもそもサーバー側でどういうことができそうか
- クラインアント側でどういう対応を行うことができるのか
GraphQLマイクロサービス
GraphQLマイクロサービスとは、GraphQLサーバーがマイクロサービスとして稼働している状態を指します。
それぞれのマイクロサービスが独自のスキーマを持ち、個別にサーバーが起動しています。
これらのサーバーに対して、フロントエンドが直接アクセスする場合、各エンドポイントを知っておく必要があり、GraphQLの利便性が損なわれることがあります。
それを解決するためにGraphQL Federationアーキテクチャを利用する場合があります
GraphQL Federation
GraphQL Federationは、いわゆるBFF(Backend for Frontend)をマイクロサービスの前段に配置し、フロントエンドがそのBFFのみを見ればよいという構成です。
これにより、複数のエンドポイントを一つのエンドポイントとして扱うことができます。各マイクロサービスのスキーマはBFFで統合され、クライアント側は統一されたスキーマを利用できます。
BFFでは、各マイクロサービスのSchemaをマージして持っておく必要があります
BFFもSchemaもマージしたくない時
BFFも配置したくなく、Schemaもマージしたくない時にフロントエンド側でどのような対応ができそうか調査していきました
フロントエンドのGraphQLクライアントとして、Relay, Apollo Client, urql, graphql-requestの4つがよく挙げられますが一旦RelayとApollo Clientについて着目しました
Relayでの対応
Relayでは、relay-compilerを用いてschemaと定義したgraphqlタグから型を生成します
例えば、relay-configをschemaごとに定義してネットワークレイヤーを特定条件時にエンドポイントを切り替える ということを試してみます
{
"artifactDirectory": "./src/__generated__/schema1",
"src": "./src",
"schema": "./graphql/schema1.graphql",
"excludes": ["**/node_modules/**", "**/__generated__/**"],
"eagerEsModules": true,
"language": "typescript"
}
{
"artifactDirectory": "./src/__generated__/schema2",
"src": "./src",
"schema": "./graphql/schema2.graphql",
"excludes": ["**/node_modules/**", "**/__generated__/**"],
"eagerEsModules": true,
"language": "typescript"
}
{
"relay:schema1": "relay-compiler ./relay.schema1.config.json",
"relay:schema2": "relay-compiler ./relay.schema2.config.json"
}
エンドポイントについては、Network作成時のfetchをカスタマイズすることで対応を検討
import { Store, RecordSource, Environment, Network, Observable } from "relay-runtime";
import type { FetchFunction, IEnvironment, RequestParameters } from "relay-runtime";
const operationKinds = {
MUTATION: 'mutation',
QUERY: 'query',
};
const isQuery = (operation: RequestParameters): boolean => operation.operationKind === operationKinds.QUERY;
const fetchFn: FetchFunction = (params, variables) => {
const host = isQuery(params) ? "http://hogehoge/query" : "http://hogehoge2/query";
const response = fetch(host, {
method: "POST",
headers: [["Content-Type", "application/json"]],
body: JSON.stringify({
query: params.text,
variables,
}),
});
return Observable.from(response.then((data) => data.json()));
};
export const createRelayEnvironment = (): IEnvironment => {
const network = Network.create(fetchFn);
const store = new Store(new RecordSource());
return new Environment({ store, network });
}
これらを設定した上でビルドするとエラーが発生しました
主な原因としてはデフォルトではbabel-plugin-relayがrelay.config.js
かrelay.config.json
を読み込みにいくためと考えられました
対策としては、Babel側の設定をoverrideすることで解決できそうですが一旦ここで断念しました
Apollo Clientでの対応
Apollo Clientの場合、型生成はApolloではなくgraphql-codegenを利用しました
ここでもschemaごとにそれぞれの設定ファイルを定義します
import { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
schema: './graphql/schema1.graphql',
documents: ['src/**/*.schema1.ts'],
generates: {
'./src/__generated__/schema1/': {
preset: 'client',
plugins: [],
presetConfig: {
gqlTagName: 'gql',
},
config: {
strictScalars: true,
scalars: {
DateTimeISO: 'string',
},
},
},
},
ignoreNoDocuments: true,
};
export default config;
import { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
schema: './graphql/schema2.graphql',
documents: ['src/**/*.schema2.ts'],
generates: {
'./src/__generated__/schema2/': {
preset: 'client',
plugins: [],
presetConfig: {
gqlTagName: 'gql',
},
config: {
strictScalars: true,
scalars: {
DateTimeISO: 'string',
},
},
},
},
ignoreNoDocuments: true,
};
export default config;
これらをそれぞれ実行します
{
"codegen:schema1": "graphql-codegen --config codegen.schema1.ts",
"codegen:schema2": "graphql-codegen --config codegen.schema2.ts"
}
Apollo Clientでは、複数のエンドポイントを扱うためのtipsを公式が上げてくれているのでそちらを参考にしました
import { HttpLink, ApolloClient, ApolloLink } from '@apollo/client';
const hogehogeLink = new HttpLink({
uri: 'https://hogehoge/query',
});
const hogehoge2Link = new HttpLink({
uri: 'https://hogehoge2/query'
});
const client = new ApolloClient({
link: ApolloLink.split(
(operation) => operation.getContext()['clientName'] === 'hogehoge2',
hogehoge2Link,
hogehogeLink
)
});
実際に使うときは、呼び出し時に指定するだけでエンドポイントを切り替えることが可能です
useQuery(query, { variables, context: { clientName: 'hogehoge2' }});
めでたく、schemaを分割しつつ、それぞれのエンドポイントに対しれリクエストを行うことができました!
おわり
クライアント側のみでschemaとエンドポイントをそれぞれ個別に扱うには、Apollo Clientが最も楽そうだと感じました
特にドキュメントが充実しているのがよかったです
複数のGraphQLエンドポイントをクライアント側で保持するユースケースがどれくらいあるかわからないですが、実装は可能ということがわかりました
しかし、エンドポイントが増えるごとに管理するエンドポイントないしschemaも増えるため、やはりBFFなりSchema stitchingなどを行うのが無難そうだなと思います
Discussion