T-CREATOR

Apollo Link レシピ集:Retry/Error/Batch/Context の組み合わせ 12 例

Apollo Link レシピ集:Retry/Error/Batch/Context の組み合わせ 12 例

Apollo Client を使った GraphQL アプリケーション開発では、さまざまなミドルウェア処理が必要になります。ネットワーク障害時のリトライ、エラーの適切なハンドリング、複数リクエストのバッチ化、認証トークンの動的な設定など、実務では複数の要件を同時に満たさなければなりません。

Apollo Link は、こうした横断的な処理を組み合わせて実現できる強力な仕組みです。しかし、組み合わせ方によっては意図しない動作になったり、順序を間違えるとエラーが発生したりすることもあります。本記事では、Retry・Error・Batch・Context の 4 つの主要な Link を使った 12 パターンのレシピを、実践的なコード例とともにご紹介します。

これらのレシピをマスターすれば、どんな要件にも柔軟に対応できるようになるでしょう。

早見表:12 個のレシピ一覧

#レシピ名使用 Link主な用途難易度
1Retry 単体Retryネットワーク障害時の自動再試行★☆☆
2Error 単体Errorエラーログ記録・通知★☆☆
3Batch 単体Batch複数リクエストの一括送信★☆☆
4Context 単体Context認証トークンの動的設定★☆☆
5Retry + ErrorRetry, Errorリトライ+エラー監視★★☆
6Retry + BatchRetry, Batchバッチリクエストの再試行★★☆
7Retry + ContextRetry, Context認証付きリトライ★★☆
8Error + BatchError, Batchバッチエラーハンドリング★★☆
9Error + ContextError, Contextコンテキスト付きエラー処理★★☆
10Batch + ContextBatch, Context認証付きバッチ処理★★☆
11Retry + Error + BatchRetry, Error, Batch高度なバッチ処理システム★★★
12全部入り全 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

この図が示すように、リクエストは上から下へ順に処理され、レスポンスは逆順に戻ってきます。

Apollo エコシステムには多数の公式・コミュニティ製 Link がありますが、実務で特に重要なのは以下の 4 つです。

#Link 名パッケージ役割
1Retry Link@apollo​/​client​/​link​/​retryネットワークエラー時の自動再試行
2Error Link@apollo​/​client​/​link​/​errorGraphQL・ネットワークエラーの検知と処理
3Batch Link@apollo​/​client​/​link​/​batch-http複数のクエリを 1 回の HTTP リクエストにまとめる
4Context Link@apollo​/​client​/​link​/​contextリクエストごとに動的にヘッダーやコンテキストを設定

これらを適切に組み合わせることで、堅牢で効率的な GraphQL クライアントを構築できます。

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 は非常に強力ですが、実務では以下のような課題に直面します。

課題 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 の組み合わせ方が身につきます。

レシピの構成

各レシピは以下の構成で説明します。

  1. ユースケース:どんな場面で使うか
  2. 使用する Link:必要なパッケージ
  3. インストール:必要な依存関係
  4. 実装コード:段階的に解説
  5. 動作確認:期待される挙動
  6. 注意点:ハマりやすいポイント

コードは TypeScript で記述し、実務ですぐに使えるレベルの品質を目指します。

具体例

それでは、12 個のレシピを順番に見ていきましょう。

ユースケース

一時的なネットワーク障害時に、自動で再試行したい場合に使います。モバイルアプリや、不安定なネットワーク環境での利用に最適です。

  • @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 は標準パッケージに含まれています。

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 エラー(クライアント側の問題)の場合はリトライしないようにしています。

typescript// GraphQL エンドポイント
const httpLink = new HttpLink({
  uri: 'https://api.example.com/graphql',
});

実際のリクエストを送信する HTTP Link を作成します。

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 番台)はリトライしても無駄です。

ユースケース

GraphQL エラーやネットワークエラーをログに記録したり、エラー監視サービス(Sentry など)に送信したりする場合に使います。

  • @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 関数を使ってエラーハンドラーを定義します。

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 レベルのエラーです。それぞれを適切にハンドリングします。

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 自体はリトライしません。

ユースケース

複数のクエリを同時に実行する際、1 回の HTTP リクエストにまとめてサーバー負荷を軽減したい場合に使います。

  • @apollo​/​client​/​link​/​batch-http

インストール

こちらも標準で含まれています。

実装:インポート

typescriptimport {
  ApolloClient,
  InMemoryCache,
} from '@apollo/client';
import { BatchHttpLink } from '@apollo/client/link/batch-http';

BatchHttpLink を使います。通常の HttpLink の代わりになります。

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 回のリクエストにまとまっていることが確認できます。

注意点

  • サーバー側もバッチリクエストに対応している必要があります。
  • バッチ化すると個別のエラーハンドリングが難しくなります。

ユースケース

リクエストごとに動的に認証トークンやヘッダーを設定したい場合に使います。ログイン機能を持つアプリケーションでは必須です。

  • @apollo​/​client​/​link​/​context

インストール

標準で含まれています。

実装:インポート

typescriptimport {
  ApolloClient,
  InMemoryCache,
  HttpLink,
} from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { ApolloLink } from '@apollo/client';

setContext 関数でコンテキストを設定します。

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 はリクエストごとに呼ばれ、動的にヘッダーを設定できます。非同期処理も可能なため、トークンのリフレッシュなどにも対応できます。

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(リトライ+エラー監視)

ユースケース

ネットワーク障害時に自動リトライしつつ、すべてのエラーをログに記録したい場合に使います。実務で最も頻出するパターンです。

  • @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 を組み合わせます。

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}`
      );
    }
  }
);

エラーを検知してログに記録します。

typescriptconst retryLink = new RetryLink({
  delay: {
    initial: 300,
    max: 5000,
    jitter: true,
  },
  attempts: {
    max: 3,
    retryIf: (error, _operation) => {
      // ネットワークエラーのみリトライ
      return !!error && !error.result;
    },
  },
});

リトライ条件を設定します。GraphQL エラー(error.result がある)の場合はリトライせず、ネットワークエラーのみリトライします。

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 が中間にあることで、リトライ可能なエラーは自動的に再試行されます。

動作確認

ネットワークを切断してクエリを実行すると、以下のような動作になります。

  1. 初回リクエスト失敗 → Error Link がエラーログ出力
  2. 300ms 後に 1 回目のリトライ → 失敗 → Error Link がログ出力
  3. 600ms 後に 2 回目のリトライ → 失敗 → Error Link がログ出力
  4. 1200ms 後に 3 回目のリトライ → 失敗 → 最終的にエラーを返す

注意点

  • Error Link を Retry Link の後に配置すると、リトライ後のエラーしかキャッチできません。
  • リトライ中のエラーログが大量になる場合は、フィルタリングを検討しましょう。

レシピ 6:Retry + Batch(バッチリクエストの再試行)

ユースケース

複数のクエリをバッチ化しつつ、ネットワーク障害時にバッチ全体を再試行したい場合に使います。

  • @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';
typescriptconst retryLink = new RetryLink({
  delay: {
    initial: 500,
    max: 3000,
    jitter: true,
  },
  attempts: {
    max: 2, // バッチの場合はリトライ回数を少なめに
  },
});

バッチリクエストは複数のクエリを含むため、リトライ回数は控えめにします。

typescriptconst batchLink = new BatchHttpLink({
  uri: 'https://api.example.com/graphql',
  batchMax: 5,
  batchInterval: 20,
});
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(認証付きリトライ)

ユースケース

認証トークンを動的に設定しつつ、トークン期限切れ時に自動リフレッシュしてリトライしたい場合に使います。

  • @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;
};

リフレッシュトークンを使って新しいアクセストークンを取得します。

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;
  }
};

トークンの有効期限をチェックし、期限切れなら自動的にリフレッシュします。

typescriptconst retryLink = new RetryLink({
  delay: { initial: 300, max: 3000 },
  attempts: {
    max: 2,
    retryIf: (error) => {
      // 401 エラー(認証エラー)の場合もリトライ
      return (
        !!error &&
        (error.statusCode === 401 || !error.result)
      );
    },
  },
});

401 エラー時にもリトライすることで、トークンリフレッシュ後に再試行できます。

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 を最初に配置することで、リトライ時にも最新のトークンが使われます。

動作確認

トークンが期限切れの状態でクエリを実行すると、以下のような流れになります。

  1. Context Link がトークン期限切れを検知してリフレッシュ
  2. 新しいトークンでリクエスト送信
  3. それでも失敗すれば Retry Link が再試行

注意点

  • トークンリフレッシュ中に複数のリクエストが発生すると、多重リフレッシュが起きる可能性があります。リフレッシュ処理をキューで管理するなどの対策が必要です。
  • リフレッシュトークン自体も期限切れの場合は、ログイン画面にリダイレクトする処理を追加しましょう。

レシピ 8:Error + Batch(バッチエラーハンドリング)

ユースケース

複数のクエリをバッチ化しつつ、個別のエラーを適切に記録・処理したい場合に使います。

  • @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';
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 エラーは個別のクエリごとに返されるため、きめ細かいハンドリングが可能です。

typescriptconst batchLink = new BatchHttpLink({
  uri: 'https://api.example.com/graphql',
  batchMax: 10,
  batchInterval: 20,
});
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(コンテキスト付きエラー処理)

ユースケース

認証情報を設定しつつ、認証エラー時には自動的にログイン画面にリダイレクトするなど、コンテキストに応じたエラー処理を行いたい場合に使います。

  • @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';
typescriptconst contextLink = setContext(
  async (operation, { headers }) => {
    const token = localStorage.getItem('authToken');

    return {
      headers: {
        ...headers,
        authorization: token ? `Bearer ${token}` : '',
      },
    };
  }
);

トークンをヘッダーに設定します。

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 エラーのエラーコードに応じて、適切な処理を行います。

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 で、複数のクエリを効率的にバッチ化したい場合に使います。ダッシュボード画面など、複数のデータを同時取得する場面で有効です。

  • @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';
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 を付与することで、ログ追跡が容易になります。

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 を使って、認証状態ごとにバッチを分けます。これにより、認証済みリクエストと未認証リクエストが混在しません。

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(高度なバッチ処理システム)

ユースケース

複数のクエリをバッチ化し、ネットワーク障害時にはリトライし、すべてのエラーを適切にログ記録する、本格的なシステムを構築したい場合に使います。

  • @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';
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
      );
    }
  }
);

バッチリクエスト内の個別エラーを記録します。

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;
    },
  },
});

バッチ全体のリトライを制御します。

typescriptconst batchLink = new BatchHttpLink({
  uri: 'https://api.example.com/graphql',
  batchMax: 10,
  batchInterval: 20,
});
typescript// 順序:Error → Retry → Batch
const link = ApolloLink.from([
  errorLink,
  retryLink,
  batchLink,
]);

const client = new ApolloClient({
  link,
  cache: new InMemoryCache(),
});

順序の理由

  1. Error Link が最初にあることで、すべてのエラー(リトライ前・後)をキャッチ
  2. Retry Link が中間にあることで、バッチ化されたリクエスト全体をリトライ
  3. 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 つのクエリがバッチ化され、失敗時にはリトライされ、すべてのエラーがログに記録されます。

動作確認

以下のような動作フローになります。

  1. 3 つのクエリが 20ms 以内に実行される
  2. Batch Link が 1 つのリクエストにまとめる
  3. ネットワークエラー発生
  4. Error Link がエラーログ出力
  5. Retry Link が 500ms 後にリトライ
  6. リトライ成功 → データ取得完了

注意点

  • バッチ全体がリトライされるため、一部のクエリだけ失敗していても全体が再実行されます。
  • リトライ回数を多くしすぎると、大量のクエリが何度も実行されることになり、サーバー負荷が高まります。
  • Error Link が大量のログを出力する場合は、ログレベルやサンプリングレートを調整しましょう。

レシピ 12:全部入り(Retry + Error + Batch + Context)

ユースケース

エンタープライズ向けの本格的な GraphQL クライアントを構築する場合に使います。認証、バッチ化、リトライ、エラー監視のすべてを統合した完全な構成です。

  • @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 をすべて使います。

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 の付与を行います。

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);
      }
    }
  }
);

詳細なエラーログとエラー監視サービスへの送信を行います。

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;
    },
  },
});

ネットワークエラーと認証エラーをリトライ対象にします。

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;
  },
});

認証状態ごとにバッチを分離します。

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',
    },
  },
});

順序の理由

  1. Context Link:最初に認証情報とリクエスト ID を設定
  2. Error Link:すべてのエラー(リトライ前後)をキャッチ
  3. Retry Link:失敗時にリトライ(トークンリフレッシュ後の再試行も可能)
  4. 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 つのクエリが、認証付きでバッチ化され、失敗時には自動リトライされ、すべてのエラーがログに記録されます。

動作確認

以下のような完全な動作フローが実現されます。

  1. トークン期限チェック → 必要ならリフレッシュ
  2. 5 つのクエリに認証ヘッダーとリクエスト ID を付与
  3. 20ms 以内なので 1 つのバッチにまとめる
  4. ネットワークエラー発生
  5. Error Link がエラーログ出力(リクエスト ID 付き)
  6. Retry Link が 300ms 後にリトライ
  7. 再度トークンチェック(念のため)
  8. バッチリクエスト再送信
  9. 成功 → すべてのデータ取得完了

注意点

  • 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 アプリケーションが実現できるでしょう。

実装時には、必ずエラーログの内容を確認し、意図した動作になっているかを検証することをお勧めします。また、本番環境では適切なエラー監視とログ管理を行い、問題の早期発見と迅速な対応を心がけましょう。

関連リンク