Claude Media
Claude CodeでHonoのAPIを作る — app.requestで検証して完了を判定

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 /posts

hono routes はルート一覧を返します。ミドルウェアやマウントしたサブアプリも展開されるため、「ルートが登録されているか」の確認に向きます。hono request は内部で app.request() を呼びます。

メソッドとボディを付けた例です。

npx hono request /posts -X POST -d '{"message":"hello hono"}'
npx hono request /posts --trace

JSONオブジェクトを -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}}
JSONL

expect が受け入れ基準になります。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つあります。

  1. * は空白を含む任意の文字列に一致する。末尾の Bash(npm test *) は、npm test 単体にも一致する
  2. timeout や nice などの決まったラッパーは、照合の前に取り除かれる。npx は取り除かれないので、npx を含めたルールを書く
  3. && や ; でつないだ複合コマンドは、各サブコマンドが個別にルールへ一致する必要がある

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で指示します。

作らせるときの進め方

実際に頼むときは、次の順で進めると手戻りが減ります。

  1. 先にルートの一覧と入出力(ステータス、ボディ)を文章で決める
  2. テストを先に書かせ、npm test で失敗を確認する
  3. ハンドラを実装させ、テストが通るまで回させる
  4. 既存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本追加させるところから始められます。

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