Apollo Link レシピ集:Retry/Error/Batch/Context の組み合わせ 12 例
Apollo Client を使った GraphQL アプリケーション開発では、さまざまなミドルウェア処理が必要になります。ネットワーク障害時のリトライ、エラーの適切なハンドリング、複数リクエストのバッチ化、認証トークンの動的な設定など、実務では複数の要件を同時に満たさなければなりません。
Apollo Link は、こうした横断的な処理を組み合わせて実現できる強力な仕組みです。しかし、組み合わせ方によっては意図しない動作になったり、順序を間違えるとエラーが発生したりすることもあります。本記事では、Retry・Error・Batch・Context の 4 つの主要な Link を使った 12 パターンのレシピを、実践的なコード例とともにご紹介します。
これらのレシピをマスターすれば、どんな要件にも柔軟に対応できるようになるでしょう。
早見表:12 個のレシピ一覧
| # | レシピ名 | 使用 Link | 主な用途 | 難易度 |
|---|---|---|---|---|
| 1 | Retry 単体 | Retry | ネットワーク障害時の自動再試行 | ★☆☆ |
| 2 | Error 単体 | Error | エラーログ記録・通知 | ★☆☆ |
| 3 | Batch 単体 | Batch | 複数リクエストの一括送信 | ★☆☆ |
| 4 | Context 単体 | Context | 認証トークンの動的設定 | ★☆☆ |
| 5 | Retry + Error | Retry, Error | リトライ+エラー監視 | ★★☆ |
| 6 | Retry + Batch | Retry, Batch | バッチリクエストの再試行 | ★★☆ |
| 7 | Retry + Context | Retry, Context | 認証付きリトライ | ★★☆ |
| 8 | Error + Batch | Error, Batch | バッチエラーハンドリング | ★★☆ |
| 9 | Error + Context | Error, Context | コンテキスト付きエラー処理 | ★★☆ |
| 10 | Batch + Context | Batch, Context | 認証付きバッチ処理 | ★★☆ |
| 11 | Retry + Error + Batch | Retry, Error, Batch | 高度なバッチ処理システム | ★★★ |
| 12 | 全部入り | 全 Link | エンタープライズ向け完全構成 | ★★★ |
背景
Apollo Link とは
Apollo Link は、Apollo Client における ミドルウェアチェーン の仕組みです。GraphQL リクエストが送信される前後に、さまざまな処理を挿入できます。
Express.js のミドルウェアや Redux のミドルウェアと同じように、複数の Link を連鎖(チェーン)させることで、リクエスト処理をモジュール化できるのが特徴です。
以下の図は、Apollo Link のデータフロー全体像を示しています。
mermaidflowchart LR
component["React<br/>コンポーネント"] -->|クエリ実行| apolloClient["Apollo Client"]
apolloClient -->|リクエスト| linkChain["Link チェーン"]
subgraph linkChain["Link チェーン"]
direction TB
contextLink["Context Link<br/>認証情報付与"]
errorLink["Error Link<br/>エラー検知"]
retryLink["Retry Link<br/>再試行"]
batchLink["Batch Link<br/>バッチ化"]
contextLink --> errorLink
errorLink --> retryLink
retryLink --> batchLink
end
linkChain -->|HTTP| server["GraphQL<br/>サーバー"]
server -->|レスポンス| linkChain
linkChain -->|データ| component
この図が示すように、リクエストは上から下へ順に処理され、レスポンスは逆順に戻ってきます。
主要な 4 つの Link
Apollo エコシステムには多数の公式・コミュニティ製 Link がありますが、実務で特に重要なのは以下の 4 つです。
| # | Link 名 | パッケージ | 役割 |
|---|---|---|---|
| 1 | Retry Link | @apollo/client/link/retry | ネットワークエラー時の自動再試行 |
| 2 | Error Link | @apollo/client/link/error | GraphQL・ネットワークエラーの検知と処理 |
| 3 | Batch Link | @apollo/client/link/batch-http | 複数のクエリを 1 回の HTTP リクエストにまとめる |
| 4 | Context Link | @apollo/client/link/context | リクエストごとに動的にヘッダーやコンテキストを設定 |
これらを適切に組み合わせることで、堅牢で効率的な GraphQL クライアントを構築できます。
Link の連鎖順序
Link を連鎖させるときは、順序が非常に重要 です。一般的な推奨順序は以下のとおりです。
mermaidflowchart TB
start["リクエスト開始"] --> context["1. Context Link<br/>認証トークン設定"]
context --> error["2. Error Link<br/>エラー監視開始"]
error --> retry["3. Retry Link<br/>リトライ制御"]
retry --> batch["4. Batch Link<br/>バッチ化"]
batch --> http["5. HTTP Link<br/>送信"]
http --> response["レスポンス"]
この順序により、以下のような処理フローが実現されます。
- Context でトークンを付与
- Error でエラーを監視
- Retry でリトライを制御
- Batch でリクエストをまとめる
- HTTP で実際に送信
順序を間違えると、たとえば「バッチ化された後にリトライしようとしてエラーになる」といった問題が発生します。
課題
Apollo Link 組み合わせの難しさ
Apollo Link は非常に強力ですが、実務では以下のような課題に直面します。
課題 1:順序依存性がわかりにくい
Link の連鎖順序を間違えると、期待した動作にならないことがあります。たとえば、Batch Link の後に Retry Link を配置すると、バッチ化されたリクエスト全体が再試行されてしまい、個別のクエリごとのリトライができません。
課題 2:組み合わせパターンが多すぎる
4 つの Link だけでも、組み合わせパターンは理論上 15 通り(単体 4 + 2 つ組み 6 + 3 つ組み 4 + 4 つ組み 1)あります。それぞれのユースケースを理解し、適切に選択するのは容易ではありません。
課題 3:公式ドキュメントに実践例が少ない
Apollo の公式ドキュメントには個別の Link の説明はありますが、「この要件にはどの Link をどう組み合わせるべきか」という実践的なレシピが不足しています。
課題 4:デバッグが難しい
複数の Link を連鎖させると、どの Link でエラーが発生したのか、リクエストがどう変換されたのかが見えにくくなります。
以下の図は、Link チェーンでのエラー伝播の複雑さを示しています。
mermaidflowchart TB
query["GraphQL クエリ"] --> context["Context Link"]
context -->|認証エラー?| contextError["認証失敗"]
context --> error["Error Link"]
error --> retry["Retry Link"]
retry -->|リトライ上限?| retryError["リトライ失敗"]
retry --> batch["Batch Link"]
batch --> http["HTTP Link"]
http -->|ネットワークエラー?| networkError["通信失敗"]
http -->|GraphQL エラー?| gqlError["サーバーエラー"]
http --> success["成功"]
contextError --> errorHandler["エラーハンドラー"]
retryError --> errorHandler
networkError --> errorHandler
gqlError --> errorHandler
このように、エラーが発生するポイントが多く、それぞれに適切な対処が必要です。
実務での具体的な困りごと
開発者が実際に直面する問題を、いくつか挙げてみましょう。
- 「認証トークンの有効期限切れ時に、自動でリフレッシュしてリトライしたい」
- 「バッチ処理中のエラーを、個別のクエリごとに記録したい」
- 「特定のエラーだけリトライして、それ以外はすぐに失敗させたい」
- 「本番環境でのみエラーログを外部サービスに送信したい」
これらの要件を満たすには、Link を適切に組み合わせる必要がありますが、どう実装すればよいのか迷うことが多いのです。
解決策
12 パターンのレシピで網羅的にカバー
本記事では、単体・2 つ組み・3 つ組み・全部入り の 4 カテゴリーに分けて、12 個のレシピを提供します。
カテゴリー 1:単体パターン(レシピ 1〜4)
まずは各 Link の基本的な使い方を理解します。Retry、Error、Batch、Context それぞれを単独で使う方法を学びます。
カテゴリー 2:2 つ組みパターン(レシピ 5〜10)
実務で最も頻出する組み合わせです。Retry + Error でリトライ付きエラーハンドリング、Batch + Context で認証付きバッチ処理など、6 通りの組み合わせをカバーします。
カテゴリー 3:3 つ組みパターン(レシピ 11)
Retry + Error + Batch の組み合わせで、高度なバッチ処理システムを構築します。
カテゴリー 4:全部入りパターン(レシピ 12)
4 つの Link すべてを組み合わせた、エンタープライズ向けの完全構成です。
以下の図は、これら 12 パターンの関係性を示しています。
mermaidflowchart TB
subgraph single["単体パターン"]
r1["レシピ1<br/>Retry"]
r2["レシピ2<br/>Error"]
r3["レシピ3<br/>Batch"]
r4["レシピ4<br/>Context"]
end
subgraph double["2つ組みパターン"]
r5["レシピ5<br/>Retry+Error"]
r6["レシピ6<br/>Retry+Batch"]
r7["レシピ7<br/>Retry+Context"]
r8["レシピ8<br/>Error+Batch"]
r9["レシピ9<br/>Error+Context"]
r10["レシピ10<br/>Batch+Context"]
end
subgraph triple["3つ組みパターン"]
r11["レシピ11<br/>Retry+Error+Batch"]
end
subgraph full["全部入りパターン"]
r12["レシピ12<br/>全Link統合"]
end
single --> double
double --> triple
triple --> full
各レシピは段階的に複雑になっていくため、順番に学習することで、自然と Link の組み合わせ方が身につきます。
レシピの構成
各レシピは以下の構成で説明します。
- ユースケース:どんな場面で使うか
- 使用する Link:必要なパッケージ
- インストール:必要な依存関係
- 実装コード:段階的に解説
- 動作確認:期待される挙動
- 注意点:ハマりやすいポイント
コードは TypeScript で記述し、実務ですぐに使えるレベルの品質を目指します。
具体例
それでは、12 個のレシピを順番に見ていきましょう。
レシピ 1:Retry Link 単体
ユースケース
一時的なネットワーク障害時に、自動で再試行したい場合に使います。モバイルアプリや、不安定なネットワーク環境での利用に最適です。
使用する Link
@apollo/client/link/retry
インストール
Apollo Client には標準で含まれているため、追加のインストールは不要です。
実装:インポート
typescriptimport {
ApolloClient,
InMemoryCache,
HttpLink,
} from '@apollo/client';
import { RetryLink } from '@apollo/client/link/retry';
import { ApolloLink } from '@apollo/client';
必要なモジュールをインポートします。RetryLink は標準パッケージに含まれています。
実装:Retry Link の設定
typescript// リトライ設定オブジェクト
const retryLink = new RetryLink({
delay: {
initial: 300, // 初回リトライまでの待機時間(ミリ秒)
max: 5000, // 最大待機時間(ミリ秒)
jitter: true, // ランダムな遅延を追加してサーバー負荷を分散
},
attempts: {
max: 3, // 最大リトライ回数
retryIf: (error, _operation) => {
// ネットワークエラーの場合のみリトライ
return !!error && error.statusCode !== 400;
},
},
});
delay でリトライ間隔を制御し、attempts で再試行条件を定義します。retryIf 関数では、400 エラー(クライアント側の問題)の場合はリトライしないようにしています。
実装:HTTP Link の作成
typescript// GraphQL エンドポイント
const httpLink = new HttpLink({
uri: 'https://api.example.com/graphql',
});
実際のリクエストを送信する HTTP Link を作成します。
実装:Link の連鎖と Client 作成
typescript// RetryLink と HttpLink を連鎖
const link = ApolloLink.from([retryLink, httpLink]);
// Apollo Client の作成
const client = new ApolloClient({
link,
cache: new InMemoryCache(),
});
ApolloLink.from で複数の Link を配列順に連鎖させます。リトライロジックが先、HTTP 送信が後になります。
動作確認
ネットワークを一時的に切断してクエリを実行すると、300ms、600ms、1200ms と間隔を開けて最大 3 回まで自動的にリトライされます。
注意点
- リトライ回数が多すぎるとユーザー体験が悪化します。通常は 3 回程度が適切です。
- サーバー側のエラー(500 番台)はリトライすべきですが、クライアント側のエラー(400 番台)はリトライしても無駄です。
レシピ 2:Error Link 単体
ユースケース
GraphQL エラーやネットワークエラーをログに記録したり、エラー監視サービス(Sentry など)に送信したりする場合に使います。
使用する Link
@apollo/client/link/error
インストール
こちらも Apollo Client に標準で含まれています。
実装:インポート
typescriptimport {
ApolloClient,
InMemoryCache,
HttpLink,
} from '@apollo/client';
import { onError } from '@apollo/client/link/error';
import { ApolloLink } from '@apollo/client';
onError 関数を使ってエラーハンドラーを定義します。
実装:Error Link の設定
typescript// エラーハンドラー
const errorLink = onError(
({ graphQLErrors, networkError, operation }) => {
// GraphQL エラー(サーバー側で発生したエラー)
if (graphQLErrors) {
graphQLErrors.forEach(
({ message, locations, path, extensions }) => {
console.error(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
`Extensions:`,
extensions
);
// 本番環境では外部サービスに送信
if (process.env.NODE_ENV === 'production') {
// 例:Sentry.captureException(new Error(message));
}
}
);
}
// ネットワークエラー(HTTP レベルのエラー)
if (networkError) {
console.error(
`[Network error]: ${networkError.message}`,
networkError
);
// ユーザーに通知
alert(
'ネットワークエラーが発生しました。接続を確認してください。'
);
}
// 実行されたオペレーション情報
console.log(
`Operation name: ${operation.operationName}`
);
}
);
graphQLErrors は GraphQL サーバーが返すエラー配列、networkError は HTTP レベルのエラーです。それぞれを適切にハンドリングします。
実装:Link の連鎖
typescriptconst httpLink = new HttpLink({
uri: 'https://api.example.com/graphql',
});
const link = ApolloLink.from([errorLink, httpLink]);
const client = new ApolloClient({
link,
cache: new InMemoryCache(),
});
Error Link を先に配置することで、すべてのエラーをキャッチできます。
動作確認
存在しないフィールドをクエリすると、GraphQL エラーが発生し、コンソールに詳細が出力されます。また、サーバーが停止していると、ネットワークエラーとしてアラートが表示されます。
注意点
- エラーログに機密情報が含まれないよう注意してください。
- エラーが発生してもリクエストは失敗として処理されます。Error Link 自体はリトライしません。
レシピ 3:Batch Link 単体
ユースケース
複数のクエリを同時に実行する際、1 回の HTTP リクエストにまとめてサーバー負荷を軽減したい場合に使います。
使用する Link
@apollo/client/link/batch-http
インストール
こちらも標準で含まれています。
実装:インポート
typescriptimport {
ApolloClient,
InMemoryCache,
} from '@apollo/client';
import { BatchHttpLink } from '@apollo/client/link/batch-http';
BatchHttpLink を使います。通常の HttpLink の代わりになります。
実装:Batch Link の設定
typescript// バッチ処理の設定
const batchLink = new BatchHttpLink({
uri: 'https://api.example.com/graphql',
batchMax: 10, // 1 回のバッチに含める最大クエリ数
batchInterval: 20, // クエリを待機する時間(ミリ秒)
batchKey: (operation) => {
// 同じキーのクエリをまとめる(通常は URL を返す)
return operation.getContext().uri || 'default';
},
});
batchInterval の間に実行されたクエリが、1 つのバッチにまとめられます。20ms 以内に 3 つのクエリが実行されれば、1 回の HTTP リクエストで送信されます。
実装:Client 作成
typescriptconst client = new ApolloClient({
link: batchLink,
cache: new InMemoryCache(),
});
Link の連鎖は不要で、batchLink をそのまま使います。
実装:バッチクエリの実行例
typescript// 複数のクエリを同時実行
const [users, posts, comments] = await Promise.all([
client.query({ query: GET_USERS }),
client.query({ query: GET_POSTS }),
client.query({ query: GET_COMMENTS }),
]);
この 3 つのクエリは、1 回の HTTP リクエストにまとめられてサーバーに送信されます。
動作確認
開発者ツールのネットワークタブを見ると、3 つのクエリが 1 回のリクエストにまとまっていることが確認できます。
注意点
- サーバー側もバッチリクエストに対応している必要があります。
- バッチ化すると個別のエラーハンドリングが難しくなります。
レシピ 4:Context Link 単体
ユースケース
リクエストごとに動的に認証トークンやヘッダーを設定したい場合に使います。ログイン機能を持つアプリケーションでは必須です。
使用する Link
@apollo/client/link/context
インストール
標準で含まれています。
実装:インポート
typescriptimport {
ApolloClient,
InMemoryCache,
HttpLink,
} from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { ApolloLink } from '@apollo/client';
setContext 関数でコンテキストを設定します。
実装:Context Link の設定
typescript// 認証トークンを取得する関数
const getAuthToken = (): string | null => {
// localStorage や Cookie から取得
return localStorage.getItem('authToken');
};
// Context Link でヘッダーを設定
const contextLink = setContext(
async (operation, { headers }) => {
const token = getAuthToken();
return {
headers: {
...headers,
// トークンがあれば Authorization ヘッダーに設定
authorization: token ? `Bearer ${token}` : '',
// カスタムヘッダーも追加可能
'x-custom-header': 'custom-value',
},
};
}
);
setContext はリクエストごとに呼ばれ、動的にヘッダーを設定できます。非同期処理も可能なため、トークンのリフレッシュなどにも対応できます。
実装:Link の連鎖
typescriptconst httpLink = new HttpLink({
uri: 'https://api.example.com/graphql',
});
const link = ApolloLink.from([
contextLink, // 先にコンテキストを設定
httpLink, // その後 HTTP リクエスト
]);
const client = new ApolloClient({
link,
cache: new InMemoryCache(),
});
Context Link を最初に配置することで、すべてのリクエストにヘッダーが付与されます。
動作確認
開発者ツールで HTTP リクエストを確認すると、Authorization: Bearer xxx ヘッダーが追加されていることがわかります。
注意点
- トークンの取得に失敗した場合のエラーハンドリングを忘れずに。
- Context Link は非同期処理が可能ですが、パフォーマンスに影響するため、重い処理は避けましょう。
レシピ 5:Retry + Error(リトライ+エラー監視)
ユースケース
ネットワーク障害時に自動リトライしつつ、すべてのエラーをログに記録したい場合に使います。実務で最も頻出するパターンです。
使用する Link
@apollo/client/link/retry@apollo/client/link/error
実装:インポート
typescriptimport {
ApolloClient,
InMemoryCache,
HttpLink,
ApolloLink,
} from '@apollo/client';
import { RetryLink } from '@apollo/client/link/retry';
import { onError } from '@apollo/client/link/error';
2 つの Link を組み合わせます。
実装:Error Link の設定
typescriptconst errorLink = onError(
({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message, extensions }) => {
console.error(
`[GraphQL error]: ${message}`,
extensions
);
});
}
if (networkError) {
console.error(
`[Network error]: ${networkError.message}`
);
}
}
);
エラーを検知してログに記録します。
実装:Retry Link の設定
typescriptconst retryLink = new RetryLink({
delay: {
initial: 300,
max: 5000,
jitter: true,
},
attempts: {
max: 3,
retryIf: (error, _operation) => {
// ネットワークエラーのみリトライ
return !!error && !error.result;
},
},
});
リトライ条件を設定します。GraphQL エラー(error.result がある)の場合はリトライせず、ネットワークエラーのみリトライします。
実装:Link の連鎖(重要)
typescriptconst httpLink = new HttpLink({
uri: 'https://api.example.com/graphql',
});
// 順序が重要:Error → Retry → HTTP
const link = ApolloLink.from([
errorLink,
retryLink,
httpLink,
]);
const client = new ApolloClient({
link,
cache: new InMemoryCache(),
});
順序の理由:Error Link を最初に配置することで、リトライ前後のすべてのエラーをキャッチできます。Retry Link が中間にあることで、リトライ可能なエラーは自動的に再試行されます。
動作確認
ネットワークを切断してクエリを実行すると、以下のような動作になります。
- 初回リクエスト失敗 → Error Link がエラーログ出力
- 300ms 後に 1 回目のリトライ → 失敗 → Error Link がログ出力
- 600ms 後に 2 回目のリトライ → 失敗 → Error Link がログ出力
- 1200ms 後に 3 回目のリトライ → 失敗 → 最終的にエラーを返す
注意点
- Error Link を Retry Link の後に配置すると、リトライ後のエラーしかキャッチできません。
- リトライ中のエラーログが大量になる場合は、フィルタリングを検討しましょう。
レシピ 6:Retry + Batch(バッチリクエストの再試行)
ユースケース
複数のクエリをバッチ化しつつ、ネットワーク障害時にバッチ全体を再試行したい場合に使います。
使用する Link
@apollo/client/link/retry@apollo/client/link/batch-http
実装:インポート
typescriptimport {
ApolloClient,
InMemoryCache,
ApolloLink,
} from '@apollo/client';
import { RetryLink } from '@apollo/client/link/retry';
import { BatchHttpLink } from '@apollo/client/link/batch-http';
実装:Retry Link の設定
typescriptconst retryLink = new RetryLink({
delay: {
initial: 500,
max: 3000,
jitter: true,
},
attempts: {
max: 2, // バッチの場合はリトライ回数を少なめに
},
});
バッチリクエストは複数のクエリを含むため、リトライ回数は控えめにします。
実装:Batch Link の設定
typescriptconst batchLink = new BatchHttpLink({
uri: 'https://api.example.com/graphql',
batchMax: 5,
batchInterval: 20,
});
実装:Link の連鎖
typescript// 順序:Retry → Batch
const link = ApolloLink.from([retryLink, batchLink]);
const client = new ApolloClient({
link,
cache: new InMemoryCache(),
});
順序の理由:Retry Link を先に配置することで、バッチ化されたリクエスト全体がリトライ対象になります。
動作確認
3 つのクエリを同時実行すると、1 つのバッチリクエストにまとめられます。ネットワークエラーが発生すると、バッチ全体が再試行されます。
注意点
- バッチ全体がリトライされるため、1 つのクエリだけ失敗している場合でも、すべてのクエリが再実行されます。
- パフォーマンスへの影響を考慮し、リトライ回数は少なめに設定しましょう。
レシピ 7:Retry + Context(認証付きリトライ)
ユースケース
認証トークンを動的に設定しつつ、トークン期限切れ時に自動リフレッシュしてリトライしたい場合に使います。
使用する Link
@apollo/client/link/retry@apollo/client/link/context
実装:インポート
typescriptimport {
ApolloClient,
InMemoryCache,
HttpLink,
ApolloLink,
} from '@apollo/client';
import { RetryLink } from '@apollo/client/link/retry';
import { setContext } from '@apollo/client/link/context';
実装:トークンリフレッシュ関数
typescript// トークンをリフレッシュする関数
const refreshAuthToken = async (): Promise<string> => {
const refreshToken = localStorage.getItem('refreshToken');
const response = await fetch(
'https://api.example.com/auth/refresh',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
}
);
const { accessToken } = await response.json();
localStorage.setItem('authToken', accessToken);
return accessToken;
};
リフレッシュトークンを使って新しいアクセストークンを取得します。
実装:Context Link の設定
typescriptconst contextLink = setContext(
async (operation, { headers }) => {
let token = localStorage.getItem('authToken');
// トークンが期限切れの場合はリフレッシュ
const isExpired = checkTokenExpiration(token);
if (isExpired) {
token = await refreshAuthToken();
}
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
},
};
}
);
// トークン期限チェック関数(簡易版)
const checkTokenExpiration = (
token: string | null
): boolean => {
if (!token) return true;
// JWT の場合は payload をデコードして exp をチェック
try {
const payload = JSON.parse(atob(token.split('.')[1]));
return payload.exp * 1000 < Date.now();
} catch {
return true;
}
};
トークンの有効期限をチェックし、期限切れなら自動的にリフレッシュします。
実装:Retry Link の設定
typescriptconst retryLink = new RetryLink({
delay: { initial: 300, max: 3000 },
attempts: {
max: 2,
retryIf: (error) => {
// 401 エラー(認証エラー)の場合もリトライ
return (
!!error &&
(error.statusCode === 401 || !error.result)
);
},
},
});
401 エラー時にもリトライすることで、トークンリフレッシュ後に再試行できます。
実装:Link の連鎖
typescriptconst httpLink = new HttpLink({
uri: 'https://api.example.com/graphql',
});
// 順序:Context → Retry → HTTP
const link = ApolloLink.from([
contextLink,
retryLink,
httpLink,
]);
const client = new ApolloClient({
link,
cache: new InMemoryCache(),
});
順序の理由:Context Link を最初に配置することで、リトライ時にも最新のトークンが使われます。
動作確認
トークンが期限切れの状態でクエリを実行すると、以下のような流れになります。
- Context Link がトークン期限切れを検知してリフレッシュ
- 新しいトークンでリクエスト送信
- それでも失敗すれば Retry Link が再試行
注意点
- トークンリフレッシュ中に複数のリクエストが発生すると、多重リフレッシュが起きる可能性があります。リフレッシュ処理をキューで管理するなどの対策が必要です。
- リフレッシュトークン自体も期限切れの場合は、ログイン画面にリダイレクトする処理を追加しましょう。
レシピ 8:Error + Batch(バッチエラーハンドリング)
ユースケース
複数のクエリをバッチ化しつつ、個別のエラーを適切に記録・処理したい場合に使います。
使用する Link
@apollo/client/link/error@apollo/client/link/batch-http
実装:インポート
typescriptimport {
ApolloClient,
InMemoryCache,
ApolloLink,
} from '@apollo/client';
import { onError } from '@apollo/client/link/error';
import { BatchHttpLink } from '@apollo/client/link/batch-http';
実装:Error Link の設定
typescriptconst errorLink = onError(
({ graphQLErrors, networkError, operation }) => {
if (graphQLErrors) {
graphQLErrors.forEach(
({ message, path, extensions }) => {
// どのクエリで発生したエラーかを特定
console.error(
`[Batch GraphQL error] Operation: ${operation.operationName}`,
`Path: ${path}`,
`Message: ${message}`,
extensions
);
// エラーごとに個別処理
if (extensions?.code === 'UNAUTHENTICATED') {
console.warn('認証エラーが発生しました');
}
}
);
}
if (networkError) {
console.error('[Batch Network error]:', networkError);
}
}
);
バッチリクエストでも、GraphQL エラーは個別のクエリごとに返されるため、きめ細かいハンドリングが可能です。
実装:Batch Link の設定
typescriptconst batchLink = new BatchHttpLink({
uri: 'https://api.example.com/graphql',
batchMax: 10,
batchInterval: 20,
});
実装:Link の連鎖
typescript// 順序:Error → Batch
const link = ApolloLink.from([errorLink, batchLink]);
const client = new ApolloClient({
link,
cache: new InMemoryCache(),
});
順序の理由:Error Link を先に配置することで、バッチ化されたすべてのクエリのエラーをキャッチできます。
動作確認
3 つのクエリをバッチ実行し、そのうち 1 つでエラーが発生すると、そのクエリのエラーだけがログに記録されます。他の 2 つは正常にデータを取得できます。
注意点
- バッチ化されたリクエストでは、一部のクエリだけ失敗することがあります。アプリケーション側で部分的なデータ取得に対応する必要があります。
- ネットワークエラーが発生すると、バッチ全体が失敗します。
レシピ 9:Error + Context(コンテキスト付きエラー処理)
ユースケース
認証情報を設定しつつ、認証エラー時には自動的にログイン画面にリダイレクトするなど、コンテキストに応じたエラー処理を行いたい場合に使います。
使用する Link
@apollo/client/link/error@apollo/client/link/context
実装:インポート
typescriptimport {
ApolloClient,
InMemoryCache,
HttpLink,
ApolloLink,
} from '@apollo/client';
import { onError } from '@apollo/client/link/error';
import { setContext } from '@apollo/client/link/context';
実装:Context Link の設定
typescriptconst contextLink = setContext(
async (operation, { headers }) => {
const token = localStorage.getItem('authToken');
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
},
};
}
);
トークンをヘッダーに設定します。
実装:Error Link の設定(認証エラー対応)
typescriptconst errorLink = onError(
({ graphQLErrors, networkError, operation }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message, extensions }) => {
console.error(
`[GraphQL error]: ${message}`,
extensions
);
// 認証エラーの場合
if (extensions?.code === 'UNAUTHENTICATED') {
console.warn(
'認証エラー:ログインページにリダイレクトします'
);
// トークンをクリア
localStorage.removeItem('authToken');
localStorage.removeItem('refreshToken');
// ログインページにリダイレクト
window.location.href = '/login';
}
// 権限エラーの場合
if (extensions?.code === 'FORBIDDEN') {
console.warn(
'権限エラー:アクセス権限がありません'
);
alert('この操作を実行する権限がありません');
}
});
}
if (networkError) {
console.error(
`[Network error]: ${networkError.message}`
);
}
}
);
GraphQL エラーのエラーコードに応じて、適切な処理を行います。
実装:Link の連鎖
typescriptconst httpLink = new HttpLink({
uri: 'https://api.example.com/graphql',
});
// 順序:Context → Error → HTTP
const link = ApolloLink.from([
contextLink,
errorLink,
httpLink,
]);
const client = new ApolloClient({
link,
cache: new InMemoryCache(),
});
順序の理由:Context Link を最初に配置して認証情報を設定し、Error Link でその結果のエラーをハンドリングします。
動作確認
無効なトークンでクエリを実行すると、UNAUTHENTICATED エラーが発生し、自動的にログインページにリダイレクトされます。
注意点
- リダイレクト前にユーザーに確認メッセージを表示したい場合は、
confirm関数を使います。 - SPA の場合は、
window.location.hrefではなく、React Router などのナビゲーション機能を使いましょう。
レシピ 10:Batch + Context(認証付きバッチ処理)
ユースケース
認証が必要な API で、複数のクエリを効率的にバッチ化したい場合に使います。ダッシュボード画面など、複数のデータを同時取得する場面で有効です。
使用する Link
@apollo/client/link/batch-http@apollo/client/link/context
実装:インポート
typescriptimport {
ApolloClient,
InMemoryCache,
ApolloLink,
} from '@apollo/client';
import { BatchHttpLink } from '@apollo/client/link/batch-http';
import { setContext } from '@apollo/client/link/context';
実装:Context Link の設定
typescriptconst contextLink = setContext(
async (operation, { headers }) => {
const token = localStorage.getItem('authToken');
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
'x-request-id': generateRequestId(), // リクエスト ID も追加
},
};
}
);
// ユニークなリクエスト ID を生成
const generateRequestId = (): string => {
return `req_${Date.now()}_${Math.random()
.toString(36)
.substr(2, 9)}`;
};
バッチリクエストごとにユニークな ID を付与することで、ログ追跡が容易になります。
実装:Batch Link の設定
typescriptconst batchLink = new BatchHttpLink({
uri: 'https://api.example.com/graphql',
batchMax: 5,
batchInterval: 20,
// バッチキーを認証状態で分ける
batchKey: (operation) => {
const context = operation.getContext();
return context.headers?.authorization || 'anonymous';
},
});
batchKey を使って、認証状態ごとにバッチを分けます。これにより、認証済みリクエストと未認証リクエストが混在しません。
実装:Link の連鎖
typescript// 順序:Context → Batch
const link = ApolloLink.from([contextLink, batchLink]);
const client = new ApolloClient({
link,
cache: new InMemoryCache(),
});
順序の理由:Context Link を先に配置することで、バッチ化される前にすべてのリクエストに認証情報が付与されます。
動作確認
ログイン後に複数のクエリを実行すると、すべてのクエリに Authorization ヘッダーが付いた状態で、1 つのバッチリクエストにまとめられます。
注意点
- バッチ内のすべてのクエリが同じ認証状態である必要があります。
batchKeyで適切に分離しましょう。 - サーバー側がバッチリクエストの認証をサポートしているか確認してください。
レシピ 11:Retry + Error + Batch(高度なバッチ処理システム)
ユースケース
複数のクエリをバッチ化し、ネットワーク障害時にはリトライし、すべてのエラーを適切にログ記録する、本格的なシステムを構築したい場合に使います。
使用する Link
@apollo/client/link/retry@apollo/client/link/error@apollo/client/link/batch-http
実装:インポート
typescriptimport {
ApolloClient,
InMemoryCache,
ApolloLink,
} from '@apollo/client';
import { RetryLink } from '@apollo/client/link/retry';
import { onError } from '@apollo/client/link/error';
import { BatchHttpLink } from '@apollo/client/link/batch-http';
実装:Error Link の設定
typescriptconst errorLink = onError(
({ graphQLErrors, networkError, operation }) => {
const operationName = operation.operationName;
if (graphQLErrors) {
graphQLErrors.forEach(
({ message, path, extensions }) => {
console.error(
`[Batch Error] Operation: ${operationName}, Path: ${path}, Message: ${message}`
);
// エラー監視サービスに送信
if (process.env.NODE_ENV === 'production') {
// sendToErrorTracking({ operationName, message, path, extensions });
}
}
);
}
if (networkError) {
console.error(
`[Batch Network Error] Operation: ${operationName}`,
networkError
);
}
}
);
バッチリクエスト内の個別エラーを記録します。
実装:Retry Link の設定
typescriptconst retryLink = new RetryLink({
delay: {
initial: 500,
max: 3000,
jitter: true,
},
attempts: {
max: 2, // バッチなので控えめに
retryIf: (error, operation) => {
// ネットワークエラーのみリトライ
const isNetworkError = !!error && !error.result;
if (isNetworkError) {
console.log(
`[Retry] Retrying batch operation: ${operation.operationName}`
);
return true;
}
return false;
},
},
});
バッチ全体のリトライを制御します。
実装:Batch Link の設定
typescriptconst batchLink = new BatchHttpLink({
uri: 'https://api.example.com/graphql',
batchMax: 10,
batchInterval: 20,
});
実装:Link の連鎖(順序が重要)
typescript// 順序:Error → Retry → Batch
const link = ApolloLink.from([
errorLink,
retryLink,
batchLink,
]);
const client = new ApolloClient({
link,
cache: new InMemoryCache(),
});
順序の理由:
- Error Link が最初にあることで、すべてのエラー(リトライ前・後)をキャッチ
- Retry Link が中間にあることで、バッチ化されたリクエスト全体をリトライ
- Batch Link が最後にあることで、リトライされるのはバッチ化後のリクエスト
実装:実際の使用例
typescript// 複数のクエリを同時実行
const fetchDashboardData = async () => {
try {
const [userData, postsData, analyticsData] =
await Promise.all([
client.query({ query: GET_USER_PROFILE }),
client.query({ query: GET_RECENT_POSTS }),
client.query({ query: GET_ANALYTICS }),
]);
return { userData, postsData, analyticsData };
} catch (error) {
console.error('Dashboard data fetch failed:', error);
throw error;
}
};
3 つのクエリがバッチ化され、失敗時にはリトライされ、すべてのエラーがログに記録されます。
動作確認
以下のような動作フローになります。
- 3 つのクエリが 20ms 以内に実行される
- Batch Link が 1 つのリクエストにまとめる
- ネットワークエラー発生
- Error Link がエラーログ出力
- Retry Link が 500ms 後にリトライ
- リトライ成功 → データ取得完了
注意点
- バッチ全体がリトライされるため、一部のクエリだけ失敗していても全体が再実行されます。
- リトライ回数を多くしすぎると、大量のクエリが何度も実行されることになり、サーバー負荷が高まります。
- Error Link が大量のログを出力する場合は、ログレベルやサンプリングレートを調整しましょう。
レシピ 12:全部入り(Retry + Error + Batch + Context)
ユースケース
エンタープライズ向けの本格的な GraphQL クライアントを構築する場合に使います。認証、バッチ化、リトライ、エラー監視のすべてを統合した完全な構成です。
使用する Link
@apollo/client/link/retry@apollo/client/link/error@apollo/client/link/batch-http@apollo/client/link/context
実装:インポート
typescriptimport {
ApolloClient,
InMemoryCache,
ApolloLink,
} from '@apollo/client';
import { RetryLink } from '@apollo/client/link/retry';
import { onError } from '@apollo/client/link/error';
import { BatchHttpLink } from '@apollo/client/link/batch-http';
import { setContext } from '@apollo/client/link/context';
4 つの Link をすべて使います。
実装:Context Link の設定
typescript// トークンリフレッシュ関数
const refreshAuthToken = async (): Promise<string> => {
const refreshToken = localStorage.getItem('refreshToken');
if (!refreshToken)
throw new Error('No refresh token available');
const response = await fetch(
'https://api.example.com/auth/refresh',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
}
);
if (!response.ok) throw new Error('Token refresh failed');
const { accessToken } = await response.json();
localStorage.setItem('authToken', accessToken);
return accessToken;
};
// Context Link
const contextLink = setContext(
async (operation, { headers }) => {
let token = localStorage.getItem('authToken');
// トークン期限チェック
if (token) {
try {
const payload = JSON.parse(
atob(token.split('.')[1])
);
const isExpired = payload.exp * 1000 < Date.now();
if (isExpired) {
console.log(
'[Context] Token expired, refreshing...'
);
token = await refreshAuthToken();
}
} catch (error) {
console.error(
'[Context] Token validation failed:',
error
);
token = null;
}
}
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
'x-request-id': `req_${Date.now()}_${Math.random()
.toString(36)
.substr(2, 9)}`,
},
};
}
);
トークンの自動リフレッシュとリクエスト ID の付与を行います。
実装:Error Link の設定
typescriptconst errorLink = onError(
({ graphQLErrors, networkError, operation }) => {
const { operationName } = operation;
const context = operation.getContext();
const requestId = context.headers?.['x-request-id'];
if (graphQLErrors) {
graphQLErrors.forEach(
({ message, path, extensions }) => {
const errorLog = {
type: 'GraphQL Error',
operation: operationName,
requestId,
path,
message,
code: extensions?.code,
};
console.error('[Error]', errorLog);
// 本番環境ではエラー監視サービスに送信
if (process.env.NODE_ENV === 'production') {
// sendToSentry(errorLog);
}
// 認証エラーの場合
if (extensions?.code === 'UNAUTHENTICATED') {
console.warn(
'[Error] Authentication failed, redirecting to login'
);
localStorage.clear();
window.location.href = '/login';
}
}
);
}
if (networkError) {
const errorLog = {
type: 'Network Error',
operation: operationName,
requestId,
message: networkError.message,
};
console.error('[Error]', errorLog);
if (process.env.NODE_ENV === 'production') {
// sendToSentry(errorLog);
}
}
}
);
詳細なエラーログとエラー監視サービスへの送信を行います。
実装:Retry Link の設定
typescriptconst retryLink = new RetryLink({
delay: {
initial: 300,
max: 5000,
jitter: true,
},
attempts: {
max: 3,
retryIf: (error, operation) => {
const isNetworkError = !!error && !error.result;
const is401 = error?.statusCode === 401;
// ネットワークエラーまたは 401 エラーの場合にリトライ
if (isNetworkError || is401) {
console.log(
`[Retry] Retrying ${operation.operationName}...`
);
return true;
}
return false;
},
},
});
ネットワークエラーと認証エラーをリトライ対象にします。
実装:Batch Link の設定
typescriptconst batchLink = new BatchHttpLink({
uri: 'https://api.example.com/graphql',
batchMax: 10,
batchInterval: 20,
batchKey: (operation) => {
// 認証状態ごとにバッチを分ける
const context = operation.getContext();
const authHeader =
context.headers?.authorization || 'anonymous';
return authHeader;
},
});
認証状態ごとにバッチを分離します。
実装:Link の連鎖(最重要)
typescript// 順序:Context → Error → Retry → Batch
const link = ApolloLink.from([
contextLink,
errorLink,
retryLink,
batchLink,
]);
const client = new ApolloClient({
link,
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
errorPolicy: 'all',
},
query: {
fetchPolicy: 'network-only',
errorPolicy: 'all',
},
mutate: {
errorPolicy: 'all',
},
},
});
順序の理由:
- Context Link:最初に認証情報とリクエスト ID を設定
- Error Link:すべてのエラー(リトライ前後)をキャッチ
- Retry Link:失敗時にリトライ(トークンリフレッシュ後の再試行も可能)
- Batch Link:最後にリクエストをバッチ化
この順序により、以下のような処理フローが実現されます。
mermaidsequenceDiagram
participant App as アプリ
participant Context as Context Link
participant Error as Error Link
participant Retry as Retry Link
participant Batch as Batch Link
participant Server as GraphQL サーバー
App->>Context: クエリ実行
Context->>Context: トークン確認<br/>期限切れ?
Context->>Context: リフレッシュ
Context->>Error: 認証情報付与
Error->>Retry: エラー監視開始
Retry->>Batch: リトライ準備
Batch->>Batch: 20ms 待機
Batch->>Server: バッチリクエスト送信
Server-->>Batch: レスポンス
Batch-->>Retry: 結果返却
alt ネットワークエラー
Retry->>Error: エラー通知
Error->>App: ログ記録
Retry->>Batch: リトライ実行
Batch->>Server: 再送信
Server-->>Batch: 成功
end
Batch-->>Error: 最終結果
Error-->>Context: データ返却
Context-->>App: 完了
実装:実際の使用例
typescript// ダッシュボードのデータ取得
const fetchFullDashboard = async () => {
const queries = [
client.query({ query: GET_USER_PROFILE }),
client.query({ query: GET_POSTS }),
client.query({ query: GET_ANALYTICS }),
client.query({ query: GET_NOTIFICATIONS }),
client.query({ query: GET_SETTINGS }),
];
try {
const results = await Promise.all(queries);
console.log(
'[Dashboard] All data fetched successfully'
);
return results;
} catch (error) {
console.error(
'[Dashboard] Failed to fetch data:',
error
);
throw error;
}
};
5 つのクエリが、認証付きでバッチ化され、失敗時には自動リトライされ、すべてのエラーがログに記録されます。
動作確認
以下のような完全な動作フローが実現されます。
- トークン期限チェック → 必要ならリフレッシュ
- 5 つのクエリに認証ヘッダーとリクエスト ID を付与
- 20ms 以内なので 1 つのバッチにまとめる
- ネットワークエラー発生
- Error Link がエラーログ出力(リクエスト ID 付き)
- Retry Link が 300ms 後にリトライ
- 再度トークンチェック(念のため)
- バッチリクエスト再送信
- 成功 → すべてのデータ取得完了
注意点
- Link の順序を間違えると、期待した動作にならないため、必ずこの順序を守ってください。
- 本番環境では、エラーログに機密情報が含まれないよう注意しましょう。
- トークンリフレッシュが多重実行されないよう、キューイング機構の導入を検討してください。
- バッチリクエストのサイズが大きくなりすぎないよう、
batchMaxを適切に設定しましょう。
拡張例:ログレベルの制御
typescript// 環境変数でログレベルを制御
const LOG_LEVEL = process.env.LOG_LEVEL || 'error';
const shouldLog = (
level: 'debug' | 'info' | 'warn' | 'error'
): boolean => {
const levels = ['debug', 'info', 'warn', 'error'];
return (
levels.indexOf(level) >=
levels.indexOf(LOG_LEVEL as any)
);
};
// Error Link でログレベルを適用
const errorLink = onError(
({ graphQLErrors, networkError }) => {
if (graphQLErrors && shouldLog('error')) {
graphQLErrors.forEach(({ message }) => {
console.error(`[GraphQL error]: ${message}`);
});
}
if (networkError && shouldLog('error')) {
console.error(
`[Network error]: ${networkError.message}`
);
}
}
);
環境に応じてログ出力を制御することで、開発時は詳細ログ、本番時はエラーのみといった運用が可能になります。
まとめ
Apollo Link の 4 つの主要コンポーネント(Retry、Error、Batch、Context)を組み合わせた 12 個のレシピをご紹介しました。
最も重要なポイントは、Link の連鎖順序 です。一般的な推奨順序は「Context → Error → Retry → Batch → HTTP」ですが、要件に応じて調整が必要です。単体パターンで基礎を学び、2 つ組みパターンで実務の頻出ケースを理解し、最終的には全部入りパターンでエンタープライズレベルの構成を実現できます。
それぞれのレシピは、以下のような場面で活用できます。
- レシピ 1〜4:基本的な Link の使い方を学ぶ
- レシピ 5〜10:実務で頻出する 2 つの Link の組み合わせ
- レシピ 11:高度なバッチ処理システムの構築
- レシピ 12:本格的な GraphQL クライアントの完成形
これらのレシピを参考に、皆さんのプロジェクトに最適な Apollo Link 構成を見つけてください。Link の組み合わせを適切に設計することで、堅牢で効率的な GraphQL アプリケーションが実現できるでしょう。
実装時には、必ずエラーログの内容を確認し、意図した動作になっているかを検証することをお勧めします。また、本番環境では適切なエラー監視とログ管理を行い、問題の早期発見と迅速な対応を心がけましょう。
関連リンク
articleApollo Link レシピ集:Retry/Error/Batch/Context の組み合わせ 12 例
articleApollo Client の正規化設計:`keyFields`/`typePolicies` で ID 設計を固定化
articleApollo Client のフィールドポリシー入門:read/merge で高度なキャッシュ制御を実装
articleApollo Client のキャッシュ初期化戦略:既存データ注入・rehydration・GC 設定
articleApollo のキャッシュ思想を俯瞰する:正規化・型ポリシー・部分データの取り扱い
articleApollo GraphOS を用いた安全なリリース運用:Schema Checks/Launch Darkly 的な段階公開
articleApollo Link レシピ集:Retry/Error/Batch/Context の組み合わせ 12 例
articleYarn を Classic から Berry に移行する手順:yarn set version の正しい使い方
articleMongoDB vs PostgreSQL 実測比較:JSONB/集計/インデックスの性能と DX
articleCline で何が自動化できる?設計・実装・テスト・運用のユースケース地図
articleWeb Components で作るアクセシブルなタブ UI:キーボード操作& ARIA 完備
articleVue.js 可観測性:Sentry/OpenTelemetry/Web Vitals で UX を数値化
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来