Claude Media
Claude CodeでNestJSアプリを開発する手順 — e2eテストで検証する

Claude CodeでNestJSアプリを開発する手順 — e2eテストで検証する

Claude CodeでNestJSアプリを作るときの、nest generateを使わせるCLAUDE.mdの書き方と、unit・e2eテストでDIの配線ミスを検出するStop hookの検証ループを解説します。

Claude CodeでNestJSアプリを作るときの要は、ファイルの雛形を nest generate に任せ、モジュールとDI(依存性注入)の配線をテストで確かめさせることです。NestはControllerやServiceをデコレーターとモジュール定義でつなぐため、コードの見た目が整っていても、providers や imports の書き漏れは動かすまで分かりません。この記事では、CLAUDE.mdへの書き方、生成コマンドの使わせ方、unitテストとe2eテストによる検証ループ、Stop hookでの自動実行を順に説明します。

NestJSで検証ループを組むときの前提

NestJSは、TypeScriptで書くNode.js向けのサーバーサイドフレームワークです。Nest CLIでプロジェクトを作ると、テストの雛形も一緒に生成されます。Nestは、アプリケーション用のe2eテストとコンポーネント用のunitテストの既定の雛形を自動で用意します。

最初に確認したいのは、プロジェクトのテストランナーです。nest new はモジュール方式を尋ね、ESM(既定)ならVitest、CommonJSならJestを使います。Claudeに「テストを書いて」と頼むと、どちらを前提にするかで vi.spyOn か jest.spyOn かが変わります。

前提

最初に確認する3点

  • テストランナー

    ESMで作った新規プロジェクトはVitest、CommonJSはJestです。

  • Node.jsのバージョン

    アプリの実行にはv20.19以降(22系ならv22.12以降)が必要です。

  • CLIの生成コマンド

    nest new と nest generate の生成処理はv22.22.3以降、v24.15以降、v26以降を要求します。

Claudeが使うシェルのNodeが古いと、生成コマンドの段階で止まります。node --version の結果をCLAUDE.mdか最初の指示に含めておくと、原因の切り分けが早くなります。

CLAUDE.mdに書くこと

CLAUDE.mdは強制される設定ではなく、Claudeが読む文脈です。「Run npm test before committing」のように、検証できる具体さで書くのが基本です。長さは1ファイル200行以内が目安です。

NestJSでは、次の3種類を書いておくと効果が出ます。生成コマンドの使用、検証コマンド、DIのルールです。

# NestJSプロジェクトの規約
 
## 雛形は nest generate で作る
- Controller / Service / Module を手で新規作成しない。`npx nest generate resource <名前>` か
  `npx nest generate module|controller|service <名前>` を使う
- 生成前に `--dry-run` で作られるファイルを確認する
- 生成した spec ファイルは消さない(`--no-spec` を付けない)
 
## 検証コマンド(変更のたびに実行)
- 型・ビルド: `npm run build`
- unit テスト: `npm run test`
- e2e テスト: `npm run test:e2e`
- 実行前に package.json の scripts と一致しているか確認する
 
## DI のルール
- 新しい Service は、所属する Module の `providers` に入れる
- 別 Module の Service を使うときは、提供側が `exports` し、利用側が `imports` する
- テストでは本物の DB に接続せず、`overrideProvider()` で差し替える

スクリプト名の test と test:e2e は、NestJSのテスト解説には出てきません。生成されたプロジェクトの package.json の scripts を見て、実際の名前に合わせてください。配置の既定は、unitテストをクラスの近くに .spec または .test の接尾辞で置き、e2eテストを test ディレクトリに .e2e-spec の接尾辞で置く形です。この配置もCLAUDE.mdに1行入れると、Claudeが置き場所を迷いません。

nest generateを使わせる

手書きの雛形は、@Module() への登録忘れの原因になります。nest generate は対象のファイルを作るだけでなく、最も近いモジュールへの取り込みも行います。取り込みを止めたいときだけ --skip-import を付けます。

# 作られるファイルを確認(ファイルシステムは変更しない)
npx nest generate resource users --dry-run
 
# 問題なければ実行
npx nest generate resource users

resource はCRUDの一式(TypeScriptのみ)を作るスキーマティックで、別名は res です。--type でREST、GraphQL、マイクロサービス、WebSocketのどれにするかを選べます。Claudeに任せるなら、種類をプロンプトで明示するほうが確実です。

--dry-run は、生成と変更の内容だけを報告します。CLAUDE.mdに「まず --dry-run」と書いておけば、既存のモジュールを意図せず書き換える生成を事前に目で確認できます。

生成コマンドで押さえるオプション

  • --dry-run(-d): 変更内容を報告するだけで、ファイルは変えません
  • --no-spec: specファイルを作りません(検証ループでは付けません)
  • --skip-import: 生成物を近くのモジュールへ取り込みません
  • --flat / --no-flat: 要素専用のフォルダーを作るかどうかを切り替えます

DIの配線ミスをテストで検出させる

テストの入口は分離テスト(isolated testing)です。new CatsService() のようにクラスを手で組み立てる方式で、DIを通らないため、Nest固有の配線は何も確かめません。Claudeが書いたテストがこの形だけだと、モジュール登録の漏れは素通りします。

配線を確かめるには、@nestjs/testing の Test.createTestingModule() を使います。

const moduleRef = await Test.createTestingModule({
  controllers: [CatsController],
  providers: [CatsService],
}).compile();
 
catsController = moduleRef.get(CatsController);

この引数は @Module() に渡すメタデータと同じ形です。compile() は、main.ts での NestFactory.create() と同様に、モジュールとその依存を組み立てます。そのため、Serviceが providers に無い、あるいは必要な依存が解決できない状態は、リクエストを投げる前の compile() の段階で表に出る形になります。

もう一段上のe2eテストでは、Supertestを使ってHTTPリクエストを模擬します。createNestApplication() で実行環境を作り、app.init() したうえで request(app.getHttpServer()) に渡します。

const moduleRef = await Test.createTestingModule({
  imports: [CatsModule],
})
  .overrideProvider(CatsService)
  .useValue(catsService)
  .compile();
 
app = moduleRef.createNestApplication();
await app.init();

ここまでの例はNestJSのテスト解説にある形に沿っています。実際に生成されるテストは、プロジェクトのモジュール方式とバージョンで差が出ます。

本物の依存をどこまで差し替えるか

overrideProvider() は、DBなどの外部依存をテストダブルへ差し替える手段です。useClass、useValue、useFactory の3つで差し替え先を渡せます。ガードやパイプなど種類別の overrideGuard() もあります。

依存が多いクラスのunitテストでは、createTestingModule() に useMocker() をつなぐ方法もあります。足りない依存を、トークンを受け取るファクトリーでまとめてモックに差し替えられます。Claudeにテストを書かせるとき、依存を1つずつ手で並べさせる手間を省けます。ただし REQUEST と INQUIRER は自動モックの対象外です。

差し替えすぎると、配線そのものを確かめられなくなります。検証したいモジュールの imports は本物のままにして、外部に出ていく依存だけを差し替える。Claudeへの指示は、この線引きで書くと意図が伝わります。

確かめたいことテストの種類差し替える対象
クラス単体のロジックテストの種類unit(分離テスト)差し替える対象依存先のクラス
モジュールの providers / imports の整合テストの種類createTestingModule() + compile()差し替える対象外部I/Oだけ
ルート・パイプ・ガードを含む応答テストの種類e2e(createNestApplication())差し替える対象DBなど外部サービス

グローバルなガードを差し替えるとき

APP_GUARD で登録したガードは、そのままでは overrideProvider() で差し替えられません。登録を useClass から useExisting に変え、ガード自体を通常のプロバイダーとしても登録すると差し替えられます。認証付きAPIのe2eをClaudeに書かせると、この点でつまずきます。事前にCLAUDE.mdか指示文に書いておくと手戻りが減ります。

hooksで検証を自動実行する

CLAUDE.mdの指示は守られない場合があります。確実に走らせたい検証は、hooksに寄せます。設定は .claude/settings.json に書きます。

まず、編集のたびにフォーマットだけを自動で走らせる構成です。生成プロジェクトには lint と format のnpmスクリプトが用意されています(oxlintとPrettier)。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "npm run format >/dev/null 2>&1 || true"
          }
        ]
      }
    ]
  }
}

次に、Claudeが応答を終えるタイミングでテストを走らせるStop hookです。失敗したら終了コード2で止め、標準エラーに理由を書きます。Stop hookの終了コード2は、Claudeが止まるのを防いで作業を続けさせます。

#!/bin/bash
INPUT=$(cat)
# 自分が起こした継続中は何もしない(無限ループ防止)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0
fi
if ! npm run test:e2e >&2; then
  echo "e2e テストが失敗しています。module の providers/imports を確認して直してください" >&2
  exit 2
fi

stop_hook_active の確認は必須です。Claude Codeは、Stop hookが連続8回ブロックした時点でフックを無視します。スクリプト側でも stop_hook_active を見て抜けておけば、同じ失敗を繰り返す往復を増やさずに済みます。上限の回数は CLAUDE_CODE_STOP_HOOK_BLOCK_CAP で変更できます。

e2eテストは時間がかかります。重いと感じるなら、Stop hookではunitテストだけを走らせ、e2eはCLAUDE.mdの「機能を足したら実行」の規約に回すという分け方もあります。

Claudeに許可する範囲

検証ループを無人で回すには、実行を許可するコマンドを絞ります。権限ルールは Bash(コマンド *) の形式で書けます。

{
  "permissions": {
    "allow": [
      "Bash(npm run test*)",
      "Bash(npm run build)",
      "Bash(npx nest generate *)"
    ]
  }
}

ワイルドカードは、サブコマンドの後ろに置きます。Bash(npm run *) は npm run build や npm run test に合致し、npm install には合致しません。Bash(npm *) のように手前まで広げると npm install も通ってしまうため、npm run の後ろにワイルドカードを置くほうが安全です。

よくあるつまずき

つまずき

NestJSでClaudeが外しやすい点

  • ESMとCommonJSの取り違え

    CommonJSのJestプロジェクトにVitestのimportを書く、またはその逆。最初にランナーをCLAUDE.mdへ明記します。

  • httpAdapterがundefined

    compile() の時点ではHTTPアダプターは未作成です。アダプターが必要なテストでは createNestApplication() を使います。

  • appを閉じ忘れる

    app.init() したe2eテストでは、最後に afterAll で app.close() を呼びます。Claudeが書くテストでも同じ後始末を入れさせます。

  • spec の削除

    失敗するspecを消して通す回避を防ぐため、CLAUDE.mdに「specを削除しない」と書きます。

スコープ付きプロバイダー(リクエストスコープなど)は、get() では取れず resolve() で取得します。リクエストごとのサブツリーを取り出したいときは、先に ContextIdFactory.create() で識別子を作り、ContextIdFactory.getByRequest をその値を返すようにスパイして、resolve(CatsService, contextId) に同じ識別子を渡します。get() と resolve() のどちらをClaudeが選んだかは、テストが落ちたときの確認項目です。

まとめ

NestJSの検証ループは、生成を nest generate、配線の確認を createTestingModule().compile()、HTTPまでの確認をe2eテスト、実行の強制をStop hookに分けて考えると組みやすくなります。最初の一歩は、プロジェクトのテストランナーと package.json のスクリプト名をCLAUDE.mdに書くことです。Nuxtのように型検査を軸にする構成はClaude CodeでNuxtアプリを開発する手順、Playwrightでブラウザ側のE2Eを足す場合はPlaywrightのE2Eテストをhookで自動実行する方法で別の組み方を説明しています。生成コマンドを軸に据える点は、Artisanを使うClaude CodeでLaravelアプリを開発する手順とも共通します。

この記事を共有:XはてブLinkedIn