newmo 技術ブログ

技術で地域をカラフルに

monorepoでのApollo Client v4移行 - pnpm named catalogとLLMによる段階的移行

Apollo Client v4が2025年9月にリリースされました。エラーハンドリングの改善やバンドルサイズの削減など多くの改善が含まれていますが、破壊的変更も多く含まれています。特にmonorepo環境では、複数のアプリケーションや共有ライブラリが同じパッケージに依存しているため、一括で移行すると変更範囲が大きくなりすぎて問題の切り分けが困難になります。そのため、アプリケーション単位で段階的な移行が必要でした。

この記事では、newmoのmonorepoでApollo Client v3からv4へ移行した際に、pnpm catalogのnamed catalog機能とLLMを活用して、v3とv4を共存させながら段階的にマイグレーションを進めた方法を紹介します。

newmoのGraphQL実装の構造

マイグレーションの課題を説明する前に、newmoにおけるGraphQLの実装構造を簡単に説明します。

graph TB
    subgraph "Web Applications"
        WebApp1[Web App A]
        WebApp2[Web App B]
        WebApp3[Web App C]
        WebApp4[その他のWebアプリ]
    end

    subgraph "Generated GraphQL Clients"
        Client1[GraphQL Client A]
        Client2[GraphQL Client B]
        Client3[GraphQL Client C]
        Client4[その他のGraphQLクライアント]
    end

    subgraph "共有パッケージ"
        GraphQLCore["@newmo-app/graphql"]
        CodeGenPlugins["GraphQL CodeGen Plugins"]
    end

    ApolloClient["@apollo/client"]

    WebApp1 --> Client1
    WebApp2 --> Client2
    WebApp3 --> Client3
    WebApp4 --> Client4

    Client1 --> GraphQLCore
    Client2 --> GraphQLCore
    Client3 --> GraphQLCore
    Client4 --> GraphQLCore

    CodeGenPlugins --> Client1
    CodeGenPlugins --> Client2
    CodeGenPlugins --> Client3
    CodeGenPlugins --> Client4

    GraphQLCore --> ApolloClient
    CodeGenPlugins --> ApolloClient
    Client1 --> ApolloClient
    Client2 --> ApolloClient
    Client3 --> ApolloClient
    Client4 --> ApolloClient

newmoでは、各Webアプリケーションに対応するGraphQL Clientパッケージを使用しています。これらのGraphQL Clientは、GraphQL SchemaとOperationファイルからGraphQL Code Generatorで自動生成されます。全てのGraphQL ClientパッケージとWebアプリケーションがApollo Clientに依存しているため、Apollo Clientのバージョンアップは全体に影響します。

詳細なアーキテクチャ(Modular Monolith、Apollo Federation、Code Generationの仕組み)については、以前の記事「Go + GraphQL による Modular Monolith なシステム設計について発表しました」および「Modular Monolith + Go @ newmo」を参照してください

マイグレーションの課題

newmoのmonorepoでは、Apollo Clientを複数のアプリケーション、共有ライブラリ、GraphQL Code Generatorのプラグインなど、多くのパッケージで利用しています。これらのパッケージが相互に依存しているため、単純にバージョンを上げるだけでは移行できません。

具体的には、次のパッケージがApollo Clientに依存していました。

  • GraphQL Client(6パッケージ)
    • 各ウェブアプリケーションに対応するGraphQL Clientで、SchemaとOperationファイルから自動生成される
  • GraphQL Core(1パッケージ)
  • CodeGen Plugin(2パッケージ)
  • Webアプリケーション(5アプリケーション)

これらのパッケージを一度に更新すると、変更範囲が大きくなりすぎて問題の切り分けが困難になります。そのため、アプリケーション単位で段階的にマイグレーションを進める必要がありました。

pnpm catalogのnamed catalogを活用した共存戦略

newmoでは、パッケージのバージョン管理にpnpm catalog機能を使っています。pnpm catalogは、One Version Ruleを実現するための仕組みで、monorepo内の各パッケージのバージョンを1つに統一します。詳細は以前の記事「monorepo内でのパッケージのバージョンを1つだけに統一するOne Version Ruleをpnpm catalogで実装する」で解説しています。

pnpm catalogは、monorepo内で同じパッケージのバージョンを統一することを前提としています。しかし、今回のマイグレーションでは、named catalog機能を活用することで、v3とv4を一時的に共存させる仕組みを構築しました。named catalogを使うことで、デフォルトのカタログとは別に名前付きのカタログを定義し、パッケージごとに参照するバージョンを切り替えられます。

pnpm-workspace.yamlでは、次のようにnamed catalogを定義しました。

catalog:
  # デフォルトカタログ(v4)
  "@apollo/client": 4.0.5

catalogs:
  # named catalog for v3
  apollo-v3:
    "@apollo/client": 3.14.0  # v3マイグレーション中のパッケージ向け
    # npm aliasを使いApollo Client v3に別名をつけて、v4とv3を一つのパッケージ内で共存させる
    "@apollo/client-v3": "npm:@apollo/client@3.14.0"

デフォルトカタログにはv4を、apollo-v3というnamed catalogにはv3を定義しています。ここで注目すべきは、@apollo/client-v3というnpm aliasを使っている点です。npm aliasを使ってApollo Client v3に別名をつけることで、v4とv3を一つのパッケージ内で共存させることができます。

これにより、@newmo-app/graphqlのような共有パッケージで、@apollo/client(v4)と@apollo/client-v3(v3)の両方を同時にインポートし、v3とv4の両方に対応したコードを提供できます。

各パッケージのpackage.jsonでは、次のように参照するカタログを指定します。

{
  "dependencies": {
    "@apollo/client": "catalog:apollo-v3"
  }
}

または

{
  "dependencies": {
    "@apollo/client": "catalog:"
  }
}

catalog:だけを指定するとデフォルトカタログを参照し、catalog:apollo-v3と指定するとnamed catalogを参照します。これにより、アプリケーションやライブラリごとに、v3とv4のどちらを使うかを選択できます。

"@apollo/client-v3": "npm:@apollo/client@3.14.0" というnpm aliasを使った特殊なnamed catalogを定義しているのは、1つのパッケージがv3とv4どちらも使うというパターンがあるためです。具体的には、共有ライブラリの@newmo-app/graphqlは、両方のバージョンをサポートする必要がありました。

共有ライブラリでの両バージョン対応

@newmo-app/graphqlは、アプリケーション間で共有される共通のGraphQL関連のユーティリティを提供するパッケージです。このパッケージは、v3とv4の両方をサポートする必要がありました。

package.jsonでは、両方のバージョンを依存に含めて、v3とv4のそれぞれのエクスポートを提供しました。

{
  "dependencies": {
    "@apollo/client": "catalog:",
    "@apollo/client-v3": "catalog:apollo-v3",
    "graphql": "catalog:"
  },
  "exports": {
    ".": "./src/index.ts",
    "./GraphQLTopLevelError": "./src/GraphQLTopLevelError.ts",
    "./v3/GraphQLTopLevelError": "./src/v3/GraphQLTopLevelError.ts"
  }
}

ソースコードは、次のようなディレクトリ構造で管理しました。

lib/typescript/packages/graphql/
├── src/
│   ├── index.ts                    # v4用エントリーポイント
│   ├── GraphQLTopLevelError.ts     # v4用実装
│   └── v3/
│       ├── index.ts                # v3用エントリーポイント
│       └── GraphQLTopLevelError.ts # v3用実装

v3用のコードでは、npm alias経由でインストールした@apollo/client-v3をインポートします。

// src/v3/GraphQLTopLevelError.ts
import {
  ApolloError,
  type ApolloQueryResult,
  type FetchResult,
} from "@apollo/client-v3";

v3を使いたいアプリケーションでは、TypeScriptのpath mappingとVitestのaliasを設定して、@newmo-app/graphqlのインポートを自動的にv3版に解決します。

tsconfig.json:

{
  "compilerOptions": {
    "paths": {
      "@newmo-app/graphql/*": ["../../../lib/typescript/packages/graphql/src/v3/*"]
    }
  }
}

vitest.config.ts:

import path from "node:path";
import { defineConfig } from "vitest/config";

export default defineConfig({
  resolve: {
    alias: [
      {
        find: "@newmo-app/graphql",
        replacement: path.resolve(import.meta.dirname, "../../../lib/typescript/packages/graphql/src/v3/"),
      },
    ],
  },
});

これにより、アプリケーションコードでは@newmo-app/graphqlをインポートするだけで、自動的に適切なバージョンが使われます。

Note: How we migrated our Rush.js monorepo to Node type stripping — Calm BlogのようにNode.jsの--conditions=v3のようなExports Conditionsを使うことでaliasは不要にできる可能性はありますが、Next.jsのサポートがわからなかったのと一時的なものだったので確実に動くことがわかっていたaliasを使うことにしました。

GraphQL Code Generatorの分岐実装

newmoでは、GraphQL Code Generatorのカスタムプラグインを使ってApollo Client用のコードを生成しています。このプラグインも、v3とv4の両方に対応する必要がありました。

graphql-codegen.tsで、各GraphQLグラフがv3とv4のどちらを使うかをリストで管理します。

const ApolloClientV3_GraphNames = [
  "graphql-client-a",
  "graphql-client-b",
  "graphql-client-c",
  // その他のGraphQL Client
];

const config: CodegenConfig = {
  generates: {
    "./hooks.tsx": {
      plugins: ["@newmo-app/graphql-codegen-plugin-typescript-react-apollo"],
      config: {
        typesFile: "./graphql",
        graphName,
        useApolloClientV3: ApolloClientV3_GraphNames.includes(graphName),
      },
    },
  },
};

プラグイン側では、useApolloClientV3フラグに応じて生成するコードを分岐させます。

const plugin = {
  plugin(schema, documents, rawConfig, _info) {
    const config = normalizeConfig(rawConfig);
    if (rawConfig.useApolloClientV3) {
      return createApolloClientV3({ config, documents });
    }
    return createApolloClient({ config, documents });
  },
};

v3用のコード生成では、従来のimport文とHooksの定義を生成します。

const createApolloClientV3 = ({ config, documents }) => {
  return `/* eslint-disable */
// This file was generated by a @newmo-app/graphql-codegen-plugin-typescript-react-apollo
import * as Apollo from '@apollo/client';
// ドキュメントからHooksを生成
`;
};

v4用のコード生成では、新しいimport構文とHooksの定義を生成します。

const createApolloClient = ({ config, documents }) => {
  return `/* eslint-disable */
// This file was generated by a @newmo-app/graphql-codegen-plugin-typescript-react-apollo
import { useSuspenseQuery, useMutation } from '@apollo/client/react';
// ドキュメントからHooksを生成
`;
};

同様に、package.json生成用のプラグインも分岐を実装しています。

Apollo Client v4の破壊的変更への対応

Apollo Client v4では、いくつかの破壊的変更が含まれています。主な変更点と対応方法を説明します。

ApolloErrorの廃止

Apollo Client v4では、ApolloErrorが廃止され、CombinedGraphQLErrorsに置き換えられました。

変更前(v3)は次の通りです。

import type { ErrorLike } from "@apollo/client/errors";

const { error } = useQuery(QUERY);
if (error.graphQLErrors) {
  error.graphQLErrors.map((graphQLError) => {
    // エラー処理
  });
}

変更後(v4)は次の通りです。

import { CombinedGraphQLErrors } from "@apollo/client/errors";

const { error } = useQuery(QUERY);
if (CombinedGraphQLErrors.is(error)) {
  error.errors.map((graphQLError) => {
    // エラー処理
  });
}

ErrorLinkの使用

onError関数は廃止され、ErrorLinkクラスを直接使用するようになりました。

変更前(v3)は次の通りです。

import { onError } from "@apollo/client/link/error";

const errorLink = onError(({ graphQLErrors, networkError }) => {
  // エラー処理
});

変更後(v4)は次の通りです。

import { ErrorLink } from "@apollo/client/link/error";

const errorLink = new ErrorLink((errorResponse) => {
  // エラー処理
});

importパスの変更

いくつかのimportパスが変更されました。

// dev関連
- import { loadDevMessages, loadErrorMessages } from "@apollo/client/dev/index.js";
+ import { loadDevMessages, loadErrorMessages } from "@apollo/client/dev";

// useApolloClient
- import { useApolloClient } from "@apollo/client";
+ import { useApolloClient } from "@apollo/client/react";

ApolloClientのジェネリクスの廃止

ApolloClientのジェネリクスは廃止されました。

// 変更前(v3)
const client = new ApolloClient<unknown>({
  cache: new InMemoryCache(),
});

// 変更後(v4)
const client = new ApolloClient({
  cache: new InMemoryCache(),
});

Content-Type問題への対応

Apollo Client v4では、GraphQL over HTTPというドラフト仕様への対応として、GraphQLレスポンスのContent-Typeチェックが厳密化されました(apollographql/apollo-client#12944)。Content-Typeがapplication/graphql-response+jsonではない場合(例: application/json)、ServerErrorとして扱われ、GraphQLエラーの詳細情報(extensionsプロパティなど)にアクセスできなくなります。

newmoのバックエンドは、Content-Typeをapplication/jsonで返していました。サーバー側の変更は必要ですが、サーバーとクライアントを同時にアップデートすると変更範囲が広がりすぎるため、まずはv3と同じ挙動でv4にアップデートすることを優先しました。そこで、issueで提案されているworkaroundを参考に、Content-Typeがapplication/jsonのエラーであってもGraphQLエラーとして扱えるようにカスタムリンクを追加しました。

const isGraphQLResult = (result: unknown): result is ApolloLink.Result => {
  return (
    result !== null &&
    typeof result === "object" &&
    (Object.hasOwn(result, "data") || Object.hasOwn(result, "errors"))
  );
};

const parseServerErrorsToGraphQLResponseLink = new ApolloLink(
  (operation, forward) => {
    return new Observable((observer) => {
      const subscription = forward(operation).subscribe({
        next: (data) => observer.next(data),
        error: (error) => {
          if (!ServerError.is(error)) {
            observer.error(error);
            return;
          }
          try {
            const result = JSON.parse(error.bodyText);

            if (isGraphQLResult(result)) {
              observer.next(result);
              observer.complete();
              return;
            }
            observer.error(error);
          } catch (_jsonParseError) {
            observer.error(error);
          }
        },
        complete: () => observer.complete(),
      });

      return () => {
        if (subscription) {
          subscription.unsubscribe();
        }
      };
    });
  },
);

このカスタムリンクは、ServerErrorのbodyTextをパースしてGraphQLレスポンスに変換し、ErrorLinkなどがGraphQLエラーを適切に処理できるようにします。

リンクチェーンには、このカスタムリンクを追加します。リンクは配列の下から順に処理されます。

const client = new ApolloClient({
  link: ApolloLink.from([
    errorLink, // 3. エラーハンドリング
    parseServerErrorsToGraphQLResponseLink, // 2. ServerErrorをGraphQLレスポンスに変換
    httpLink, // 1. HTTPリクエストを実行
  ]),
  cache: new InMemoryCache(),
});

マイグレーションの手順

実際のマイグレーションは、次の4ステップで段階的に実施しました。

Step 1: v3とv4の共存環境の構築

まず、現在のリポジトリがv3を使う状態を維持しながら、v4をインストールし、共存の仕組みを構築しました。この段階では、動作はv3のままです。

Step 2: v4対応パッケージの作成

GraphQL Code Generatorプラグインと共有ライブラリにv3/v4分岐ロジックを追加しました。

Step 3: アプリケーション単位でv4へ移行

各アプリケーションを個別にv4へ移行しました。移行の手順は次の通りです。

  1. Code Generator設定でv3リストから対象グラフを削除
  2. v4用のGraphQLコードを生成
  3. package.jsonの依存関係をv4に変更
  4. TypeScript設定ファイルから共有ライブラリのaliasを削除
  5. codemodで自動変換
  6. 手動で残りの箇所を修正
  7. Content-Type対応のカスタムリンクを追加

この手順で5つのアプリケーションを順次移行しました。

マイグレーションプロンプトとLLMの活用

アプリケーションごとの移行作業では、Claude Codeを活用して効率化しました。

最初のアプリケーションでの試行錯誤

最初のアプリケーションでは、Claude Codeと手動作業を組み合わせながら移行を進めました。公式のcodemodツールで自動変換可能な部分は自動化できましたが、次のような手動対応が必要でした。

  • ErrorBoundaryコンポーネントやerror.tsxでのエラーハンドリングの変更
  • ApolloClientのインスタンス化部分の修正
  • tsconfig.jsonやvitest.config.tsの設定変更
  • Content-Type問題への対応としてカスタムリンクの追加

マイグレーションプロンプトの作成

最初のアプリケーションの移行を進めながら、移行のパターンやよくある落とし穴を見つけ次第、マイグレーションプロンプトに追加していきました。 最初のアプリケーションの移行が完了してできた、プロンプトは次のような内容になっています。

マイグレーションプロンプトの詳細

## マイグレーション手順

### 前提条件

- monorepoのルートディレクトリにいること
- 対象アプリケーションが決まっていること
- 対象アプリケーションのGraphQLグラフ名を確認していること

### Step 1: graphql-codegen.tsの更新

対象アプリケーションのGraphQLグラフ名を、ApolloClientV3_GraphNamesリストから削除します。

ファイル: `graphql-codegen.ts`

```typescript
const ApolloClientV3_GraphNames = [
  // 移行対象のグラフ名をここから削除
  // 例: "graphql-client-a", を削除
];
```

### Step 2: GraphQLコードの生成

次のコマンドを実行して、v4用のGraphQLコードを生成します。

```bash
make build-graphql-operations-typescript
```

### Step 3: package.jsonの更新

対象アプリケーションのpackage.jsonで、@apollo/clientの依存をcatalog:に変更します。

ファイル: `app/<アプリケーション名>/package.json`

変更前:
```json
{
  "dependencies": {
    "@apollo/client": "catalog:apollo-v3"
  }
}
```

変更後:
```json
{
  "dependencies": {
    "@apollo/client": "catalog:"
  }
}
```

### Step 4: TypeScript設定の更新

tsconfig.jsonから@newmo-app/graphqlのpath mappingを削除します。

ファイル: `app/<アプリケーション名>/tsconfig.json`

変更前:
```json
{
  "compilerOptions": {
    "paths": {
      "@newmo-app/graphql/*": ["../../../lib/typescript/packages/graphql/src/v3/*"]
    }
  }
}
```

変更後:
```json
{
  "compilerOptions": {
    "paths": {}
  }
}
```

pathsが空になる場合は、paths全体を削除しても構いません。

### Step 5: Vitest設定の更新

vitest.config.tsから@newmo-app/graphqlのaliasを削除します。

ファイル: `app/<アプリケーション名>/vitest.config.ts`

変更前:
```typescript
import path from "node:path";
import { defineConfig } from "vitest/config";

export default defineConfig({
  resolve: {
    alias: [
      {
        find: "@newmo-app/graphql",
        replacement: path.resolve(import.meta.dirname, "../../../lib/typescript/packages/graphql/src/v3/"),
      },
    ],
  },
});
```

変更後:
```typescript
import { defineConfig } from "vitest/config";

export default defineConfig({
  // alias設定を削除
});
```

### Step 6: codemodの実行

Apollo Client公式のcodemodツールを実行して、自動変換可能な箇所を修正します。

```bash
cd app/<アプリケーション名>

# TypeScriptファイルに対して実行
npx @apollo/client-codemod-migrate-3-to-4 --parser ts --extensions ts src

# TypeScript JSXファイルに対して実行
npx @apollo/client-codemod-migrate-3-to-4 --parser tsx --extensions tsx src
```

### Step 7: 手動修正

次の箇所は手動で修正が必要です。

#### 7.1 ErrorBoundaryコンポーネントの修正

ファイル: `app/<アプリケーション名>/src/components/ErrorBoundary.tsx`(存在する場合)

変更前:
```typescript
import type { ErrorLike } from "@apollo/client/errors";

if (error.graphQLErrors) {
  error.graphQLErrors.map((graphQLError) => {
    // エラー処理
  });
}
```

変更後:
```typescript
import { CombinedGraphQLErrors } from "@apollo/client/errors";

if (CombinedGraphQLErrors.is(error)) {
  error.errors.map((graphQLError) => {
    // エラー処理
  });
}
```

#### 7.2 error.tsxの修正

ファイル: `app/<アプリケーション名>/src/app/error.tsx`(存在する場合)

同様に、ErrorLikeからCombinedGraphQLErrorsへ変更します。

#### 7.3 ApolloClientインスタンス化の修正

ファイル: `app/<アプリケーション名>/src/lib/apollo-client.ts`(パスは異なる可能性あり)

codemodが次のようなコメントを追加しますが、使用していない機能なので削除します。

削除するコメントと設定:
```typescript
/*
Inserted by Apollo Client 3->4 migration codemod.
If you are not using the `@client` directive in your application,
you can safely remove this option.
*/
localState: new LocalState({}),

/*
Inserted by Apollo Client 3->4 migration codemod.
If you are not using the `@defer` directive in your application,
you can safely remove this option.
*/
incrementalHandler: new Defer20220824Handler()
```

これらは削除して構いません。

#### 7.4 カスタムリンクの追加

Content-Type問題に対応するため、カスタムリンクをリンクチェーンに追加します。

ファイル: `app/<アプリケーション名>/src/lib/apollo-client.ts`(パスは異なる可能性あり)

追加するコード:
```typescript
import { ApolloLink, Observable } from "@apollo/client/core";
import { ServerError } from "@apollo/client/link/utils";

const isGraphQLResult = (result: unknown): result is ApolloLink.Result => {
  return (
    result !== null &&
    typeof result === "object" &&
    (Object.hasOwn(result, "data") || Object.hasOwn(result, "errors"))
  );
};

const parseServerErrorsToGraphQLResponseLink = new ApolloLink(
  (operation, forward) => {
    return new Observable((observer) => {
      const subscription = forward(operation).subscribe({
        next: (data) => observer.next(data),
        error: (error) => {
          if (!ServerError.is(error)) {
            observer.error(error);
            return;
          }
          try {
            const result = JSON.parse(error.bodyText);

            if (isGraphQLResult(result)) {
              observer.next(result);
              observer.complete();
              return;
            }
            observer.error(error);
          } catch (_jsonParseError) {
            observer.error(error);
          }
        },
        complete: () => observer.complete(),
      });

      return () => {
        if (subscription) {
          subscription.unsubscribe();
        }
      };
    });
  },
);
```

リンクチェーンへの追加:
```typescript
const client = new ApolloClient({
  link: ApolloLink.from([
    errorLink,
    parseServerErrorsToGraphQLResponseLink, // この行を追加
    httpLink,
  ]),
  cache: new InMemoryCache(),
});
```

### Step 8: 依存関係のインストール

```bash
pnpm install
```

### Step 9: 確認

次のコマンドを実行して、問題がないか確認します。

```bash
# 型チェック
cd app/<アプリケーション名>
pnpm run typecheck

# lint
cd ../..
pnpm run eslint:web:<アプリケーション名>

# テスト(Playwrightが設定されている場合)
pnpm run test:playwright:web:<アプリケーション名>
```

型エラーやlintエラーがある場合は、手動で修正します。型チェックを実行することで、変更漏れを見つけることができます。

## よくある問題と対処法

### 問題1: graphQLErrorsプロパティにアクセスできない

エラー: `Property 'graphQLErrors' does not exist on type 'ApolloError | undefined'`

対処法: CombinedGraphQLErrors.is()でチェックしてからerrorsプロパティにアクセスする

```typescript
// 変更前
if (error.graphQLErrors) {
  error.graphQLErrors.map((graphQLError) => {
    // エラー処理
  });
}

// 変更後
import { CombinedGraphQLErrors } from "@apollo/client/errors";

if (CombinedGraphQLErrors.is(error)) {
  error.errors.map((graphQLError) => {
    // エラー処理
  });
}
```

### 問題2: ErrorLinkのインポートエラー

エラー: `Module '"@apollo/client/link/error"' has no exported member 'onError'`

対処法: onErrorの代わりにErrorLinkクラスを使用する

```typescript
// 変更前
import { onError } from "@apollo/client/link/error";

const errorLink = onError(({ graphQLErrors, networkError }) => {
  // エラー処理
});

// 変更後
import { ErrorLink } from "@apollo/client/link/error";

const errorLink = new ErrorLink((errorResponse) => {
  const { error: graphqlErrors, operation, forward } = errorResponse;
  if (CombinedGraphQLErrors.is(graphqlErrors)) {
    // エラー処理
  }
});
```

### 問題3: useApolloClientのインポートエラー

エラー: `Module '"@apollo/client"' has no exported member 'useApolloClient'`

対処法: インポートパスを変更する

```typescript
// 変更前
import { useApolloClient } from "@apollo/client";

// 変更後
import { useApolloClient } from "@apollo/client/react";
```

### 問題4: Top Level Errorのextensionsにアクセスできない

Content-Type問題により、ServerErrorとして扱われ、extensionsプロパティにアクセスできなくなります。

対処法: parseServerErrorsToGraphQLResponseLinkをリンクチェーンに追加する(Step 7.4を参照)

2つ目以降のアプリケーションでの効率化

2つ目以降のアプリケーションでは、作成したプロンプトをClaude Codeに渡すだけで移行を完了できました。

プロンプトに含めた要素として、次が特に有効でした。

  • 具体的なコマンドとその実行順序
  • コード例のbefore/after
  • よくある間違いと対処法
  • 確認方法とテスト手順

このアプローチは、他のライブラリのメジャーアップデートや大規模なリファクタリングにも適用できます。

Step 4: クリーンアップ

全てのアプリケーションの移行が完了したら、v3の分岐コードを削除しました。削除した内容は次の通りです。

  • lib/typescript/packages/graphql/src/v3/ディレクトリ
  • pnpm-workspace.yamlのapollo-v3 named catalog
  • graphql-codegen.tsのv3用のフラグ
  • GraphQL Code Generatorプラグインのv3分岐コード

まとめ

Apollo Client v3からv4へのマイグレーションを、pnpmのnamed catalogとLLMを活用して段階的に実施しました。

pnpm catalogのnamed catalogとnpm aliasを組み合わせてv3とv4を共存させ、アプリケーション単位で段階的に移行しました。最初のアプリケーションを移行しながら、移行の背景・手順・プロンプトをDesign Docとして整理しました。プロンプト完成後は、チームメンバーと協力して約1週間で全アプリケーションの移行を完了しました。

最初のアプリケーションでの学びをプロンプトとして体系化することで、2つ目以降はClaude Codeに渡すだけで移行できるようになりました。このアプローチは、同じような作業が必要な段階的なマイグレーションと相性が良く、変更点が多いライブラリのメジャーアップデートや大規模なリファクタリングでも活用できると考えています。