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 usersresource は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
fistop_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アプリを開発する手順とも共通します。