Claude CodeでHonoのAPIを作る — app.requestで検証して完了を判定
Claude CodeでHonoのAPIを作るとき、サーバを立てずにapp.requestとHono CLIで検証させる手順。Bash許可を最小にする権限設定と、Stop hookによる完了判定の例も示します。
Claude CodeでHonoのAPIを作る — app.requestで検証して完了を判定
Honoのアプリは、サーバを起動しなくても検証できます。app.request にパスを渡せば、Web標準の Response が返ってくるためです。Claude Codeに任せる開発では、この性質がそのまま効きます。npm run dev を裏で走らせたり、curl を許可したりせず、テストコマンド1つで「動いた」を判定できるからです。
この記事では、HonoのAPIを作らせる流れを3段階に分けて説明します。app.request のテスト、Hono CLIによる一発確認、そしてStop hookで完了を判定する設定です。
なぜHonoは検証ループを短くできるのか
Honoの公式ガイドは、テストの基本を「Requestを作ってHonoアプリに渡し、Responseを検証するだけ」と説明しています。そのための入り口が app.request です。
Honoではアプリ自体が Request を受けて Response を返す入り口として扱えるため、検証のために起動するプロセスもポート番号も要りません。
Claude Codeにとっては、権限の面でも違いが出ます。
| 検証方法 | 必要なBash許可 | 事前に要るもの |
|---|---|---|
devサーバを起動して curl | 必要なBash許可サーバ起動と curl | 事前に要るもの起動済みのサーバ |
app.request のテスト | 必要なBash許可テストランナーのみ | 事前に要るものテストファイル |
Hono CLIの hono request | 必要なBash許可hono request のみ | 事前に要るもの@hono/cli の導入 |
許可するコマンドが減るほど、承認の確認が減り、許可しすぎのリスクも下がります。
app.requestでテストを書かせる
テストの書き方は、Hono公式のテストガイドがそのまま土台になります。ガイドはCloudflare WorkersとVitestを例にしていますが、「基本の手順は同じ」と書かれています。
次のようなルートを持つアプリを考えます。
app.get('/posts', (c) => {
return c.text('Many posts')
})
app.post('/posts', (c) => {
return c.json({ message: 'Created' }, 201, { 'X-Custom': 'Thank you' })
})GET /posts のテストは、Honoのドキュメントの例で次の形になります。
describe('Example', () => {
test('GET /posts', async () => {
const res = await app.request('/posts')
expect(res.status).toBe(200)
expect(await res.text()).toBe('Many posts')
})
})POST でJSONを送るときは、body と headers を渡します。
test('POST /posts', async () => {
const res = await app.request('/posts', {
method: 'POST',
body: JSON.stringify({ message: 'hello hono' }),
headers: new Headers({ 'Content-Type': 'application/json' }),
})
expect(res.status).toBe(201)
expect(res.headers.get('X-Custom')).toBe('Thank you')
expect(await res.json()).toEqual({ message: 'Created' })
})multipart/form-data は FormData を body に渡します。new Request('http://localhost/posts', ...) で作った Request のインスタンスを渡すこともできます。この方法は「End-to-Endのようにテストできる」と説明しています。
環境変数やバインディングを差し替える
app.request の第3引数に渡したオブジェクトが c.env になります。Cloudflare Workersのバインディングをモックする用途です。
const MOCK_ENV = {
API_HOST: 'example.com',
DB: {
prepare: () => {
/* mocked D1 */
},
},
}
test('GET /posts', async () => {
const res = await app.request('/posts', {}, MOCK_ENV)
})DBに触るルートでも、本物のDBを用意する必要がありません。Claude Codeに「c.env はモックで渡す」と伝えておくと、外部サービスへの接続を伴わないテストになります。
型付きテストクライアントを使う場合
hono/testing の testClient() は、ルートの型に沿った呼び出しをテストで書けるヘルパーです。ただし制約があります。型推論のためにルートを new Hono().get(...) のようにインスタンスへ直接チェーンして定義する必要があり、const app = new Hono(); app.get(...) と別々に書くと型情報が得られないと注意しています。
型付きクライアントを使うなら、その点をCLAUDE.mdに書いておく価値があります。Claude Codeが後からルートを追加するとき、チェーンを崩さずに書き足せるからです。
CLAUDE.mdに書く規約
テストの書き方を毎回プロンプトで伝える代わりに、CLAUDE.mdへ短い規約として残します。次は一例です。
## テストと検証(Hono)
- HTTPの検証は `app.request` で行う。サーバ(`npm run dev`)は起動しない
- ルートを追加・変更したら、同じファイルのテストも更新する
- `c.env` が要るルートは `app.request(path, init, MOCK_ENV)` で渡す
- 型付きクライアントを使うため、ルートは `new Hono().get(...)` のチェーンで書く
- テストは `npm test` で実行し、失敗が残る間は完了と報告しない最後の行が効きます。「完了の定義」を文章で与えておくと、Claude Codeが自分でテストを回す動機になります。ただしCLAUDE.mdは指示であって強制ではありません。強制はこのあとのStop hookが担います。
Hono CLIで一発確認させる
公式サイトには、コーディングエージェント向けのHono CLIが載っています。アプリを直接読み込み、サーバを起動せずに検証できるツールです。出力はJSONで、成功は { "ok": true, "data": ... } と終了コード0、失敗は { "ok": false, "error": {...} } と終了コード1です。失敗時の error.suggestions に、次に試すことが入ります。
npm install -D @hono/cli
npx hono routes
npx hono request /postshono routes はルート一覧を返します。ミドルウェアやマウントしたサブアプリも展開されるため、「ルートが登録されているか」の確認に向きます。hono request は内部で app.request() を呼びます。
メソッドとボディを付けた例です。
npx hono request /posts -X POST -d '{"message":"hello hono"}'
npx hono request /posts --traceJSONオブジェクトを -d に渡すとJSONとして送られます。--trace を付けると、どのミドルウェアとハンドラにマッチし、どれが応答したかが出力されます。404のときに原因を探す手段として、ドキュメントにも書かれています。
hono batchで複数リクエストを順に流す
複数のリクエストをまとめて検証するなら hono batch です。JSONLの1行が1リクエストで、同じアプリのインスタンスに対して順番に実行されます。
npx hono batch - <<'JSONL'
{"path":"/posts","expect":{"status":200}}
{"method":"POST","path":"/posts","body":{"message":"hi"},"expect":{"status":201}}
JSONLexpect が受け入れ基準になります。status は完全一致で、body は部分一致です。宣言したフィールドだけが比較され、レスポンスに余分なフィールドがあっても無視されます。expect を省いた行は、2xxと3xxなら成功、4xxと5xxなら失敗です。意図して4xxを期待するときは expect.status で明示します。
出力にある failed が0になるまで再実行します。Claude Codeに渡す指示としては「failedが0になるまで直して再実行」と書けば足ります。
変更前後の差分を見る
既存のAPIを触らせるときは、hono snapshot と hono diff が使えます。
hono snapshot: 現在の挙動をbatch用のJSONL行として標準出力に出す。ファイルは書かないhono diff: 未コミットの変更が、GETルートの応答をどう変えたかを比較する。最後のコミット時点と現在の両方でアプリを動かす
リファクタリングを頼むときに便利です。changed が空なら、GETルートの応答は変わっていません。パラメータ付きルートとGET以外のルートは diff の対象外なので、hono batch で確認するよう書いています。
Cloudflareのwrangler設定があるプロジェクトでは、c.env にローカルのバインディングが入った状態でNode.js上で動きます。モックで足りるときは --no-bindings で読み込みを省けます。
Bash許可を最小にする
検証を app.request に寄せると、許可するBashコマンドも絞れます。.claude/settings.json の例です。
{
"permissions": {
"allow": [
"Bash(npm test *)",
"Bash(npx hono routes *)",
"Bash(npx hono request *)"
],
"deny": [
"Bash(npm run dev *)"
]
}
}ルールの書き方には、Claude Codeの仕様に沿った注意点が3つあります。
*は空白を含む任意の文字列に一致する。末尾のBash(npm test *)は、npm test単体にも一致するtimeoutやniceなどの決まったラッパーは、照合の前に取り除かれる。npxは取り除かれないので、npxを含めたルールを書く&&や;でつないだ複合コマンドは、各サブコマンドが個別にルールへ一致する必要がある
3つ目の詳細はClaude Codeの複合コマンドとBash権限にまとめています。
もう1つ、権限のドキュメントが警告している点があります。引数を細かく制限するBashルールは壊れやすいという点です。たとえば Bash(curl http://github.com/ *) はURLを絞る意図でも、オプションの位置やプロトコルが違えば一致しません。denyルールも、sh -c '...' や絶対パスでの呼び出しは止めません。
npm run dev のdenyは、Claude Codeが普段書く形を止める用途です。境界を保証するものではありません。ネットワークやファイルを確実に制限したいなら、サンドボックスを併用します。
Stop hookで完了を判定する
CLAUDE.mdの「失敗が残る間は完了と報告しない」は、Claude Codeの判断に委ねた指示です。確実に止めたいなら、Stop hookにテストを実行させます。Stop hookはClaudeが応答を終えようとするたびに発火します。ブロックされると、reason が次の指示としてClaudeに戻り、作業が続きます。
.claude/settings.json に追加する設定です。
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": ".claude/hooks/verify-hono.sh"
}
]
}
]
}
}.claude/hooks/verify-hono.sh の例です。テストが通れば何も出さずに終わり、失敗したときだけ末尾を reason に入れてブロックします。
#!/bin/bash
INPUT=$(cat)
# 既にStop hookで続行中なら、無限ループを避けて止める
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0
fi
if OUT=$(npm test 2>&1); then
exit 0
fi
jq -n --arg r "$(echo "$OUT" | tail -n 40)" \
'{decision: "block", reason: ("npm test が失敗しています。直して再実行してください。\n" + $r)}'押さえておく点が3つあります。
stop_hook_activeの確認が必須。Stop hookが連続8回、Claudeのツール呼び出しなしでブロックすると、Claude Codeはhookを上書きして止める。上限はCLAUDE_CODE_STOP_HOOK_BLOCK_CAPで変えられる(上限の仕様はこちら)- Stop hookはタスク完了時だけでなく、Claudeが応答を終えるたびに動く。ユーザーによる中断では動かない
- 標準出力に出すのはJSONだけにする。シェルのプロファイルが
echoを出力すると、JSONとして読まれず無視される
hookでテストを回す全般的な設計はClaude Code hooksでテストを自動実行するで比較しています。テストコマンドがNestJSのe2eなら、同じ型がNestJSアプリの開発手順にも出てきます。
この構成では、完了の条件が npm test の終了コードに一本化されます。hono batch による確認は、Honoの開発中にClaudeが反復して回す手段として、CLAUDE.mdで指示します。
作らせるときの進め方
実際に頼むときは、次の順で進めると手戻りが減ります。
- 先にルートの一覧と入出力(ステータス、ボディ)を文章で決める
- テストを先に書かせ、
npm testで失敗を確認する - ハンドラを実装させ、テストが通るまで回させる
- 既存APIを触る場合は、変更前に
hono snapshotを取り、変更後にhono batchで再確認する
手順2で失敗を見せる理由は、テストが何も検証していない状態を避けるためです。res.status を一度も見ないテストは、通っても意味がありません。
使い分けの早見表
| 目的 | 使うもの |
|---|---|
| ルートの単体検証を残す | 使うものapp.request のテスト(Vitestなど) |
| ルートが登録されているか見る | 使うものhono routes |
| 1リクエストを素早く試す | 使うものhono request |
| 複数リクエストを順番に検証する | 使うものhono batch |
| リファクタ前後でGETの応答を比べる | 使うものhono snapshot と hono diff |
| 型付きでテストを書く | 使うものtestClient()(ルートをチェーンで定義) |
RustのWeb APIでも、検証コマンドをhookに束ねる発想は変わりません。コンパイルと静的解析を軸にした例はClaude CodeでAxumのRust APIを開発するで扱っています。
まとめ
Honoの app.request は、サーバを起動せずにHTTPの挙動を検証できる仕組みです。この性質のおかげで、Claude Codeに許可するBashコマンドは npm test と hono request 程度に絞れます。完了の判定はStop hookに任せ、判定の基準を npm test の終了コードにそろえると、「動いたと言っているだけ」の状態を避けられます。まずはCLAUDE.mdに5行の規約を書き、テストを1本追加させるところから始められます。