hono-api-best-practices について
hono-api-best-practicesは、HonoとZodを使うAPIの設計、レビュー、標準化を支援するスキルです。TypeScriptバックエンドとその利用者向け契約を扱う開発者を対象とし、RESTかPOST専用の規約を選んで、ZodスキーマからOpenAPI文書を生成します。
主な機能
- 01RESTまたはPOST専用方式の選択
- 02ZodによるAPI契約定義
- 03ルートからのOpenAPI生成
- 04エラー変換とレスポンス検証の統一
- 05認証・バージョン・制限の規約
活用シーン
- 01新しいHTTPエンドポイントの設計
- 02既存APIルートのレビュー
- 03クライアントやMCP利用者向け契約の定義
- 04OpenAPI生成方法の標準化
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ marcioaltoe/ skills --skill hono-api-best-practicesインストール後、Claude Code で/hono-api-best-practicesを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/api-conventions.mdopenapi-generation.mdstyle-post-only.mdstyle-rest.md詳細
API 設計と標準(Hono + Zod)
このスキルを使用する場面
- 境界づけられたコンテキストに新しい HTTP エンドポイントを定義するとき
- 一貫性を保つために、既存の API ルートをレビュー、監査、リファクタリングするとき
- TypeScript/クリーンアーキテクチャのバックエンド向けに API 標準を確立するとき
- フロントエンド、SDK、CLI、MCP、サードパーティーの利用者向けにコントラクトを設計するとき
- 共有コントラクトパッケージ(例:
api-contracts)で API コントラクトを定義または更新するとき - 一貫したルート登録と、生成される OpenAPI ドキュメントを徹底するとき
まず API スタイルを選択する
このスキルでは、互いに排他的な 2 つの規約をサポートしています。エンドポイントの設計や監査を始める前に、プロジェクトごとに必ずどちらか 1 つを選び、同じ API の範囲内で混在させないでください。以下の共通基盤(クリーンアーキテクチャ、信頼できる情報源としての Zod、createRoute + app.openapi、エラーヘルパー、生成される OpenAPI)は、どちらのスタイルにも適用されます。異なるのはパス、メソッド、パラメーターの配置、レスポンスの形状のみです。
| スタイル | 形式 | 参照先 |
|---|---|---|
| 標準 REST | リソースパス + HTTP 動詞(GET/POST/PATCH/PUT/DELETE)。パラメーターはパス/クエリー/ボディに配置し、レスポンスではリソース/コレクションを直接返します | references/style-rest.md |
| POST のみのアクションベース | すべてのエンドポイントを POST /<context>/<entity>/<action> とします。JSON ボディのみを使用し、レスポンスには { data, message }/{ error, details } のエンベロープを使用します | references/style-post-only.md |
次の順序でスタイルを判断してください。
- プロジェクトのドキュメントを優先します。
AGENTS.md、ADR、設計ドキュメントを確認してください。これらでスタイルが指定されている場合(またはこのスキルの内容と矛盾する場合)は、プロジェクトのドキュメントを優先します。 - 既存のルートから推測します。
createRoute({を検索し、method/pathを確認してください。動詞が複数あり、/{id}パスを使っている場合は REST です。すべてのルートでmethod: "post"を使い、/<action>を末尾に含む場合は POST のみのスタイルです。 - 新規プロジェクト、または判断できない場合: プロジェクトで採用しているスタイルをユーザーに確認してください。新しい公開 API では、プロジェクトですでに POST のみのスタイルに標準化されている場合を除き、標準 REST をデフォルトにします。
スタイルを選択したら、そのスタイルの参照ファイルのみを読み、一貫して適用してください。プロジェクトがすでに一方のスタイルを採用している場合、もう一方のスタイルでエンドポイントを追加しないでください。
中核となる原則(どちらのスタイルにも適用)
- 共有の
api-contractsパッケージにコントラクトを集約する:バックエンド、フロントエンド、SDK、テスト間で共有する場合、リクエスト/レスポンスのスキーマは一元管理されたパッケージに配置します。 - Zod を唯一の信頼できる情報源とする:すべてのスキーマで、ランタイム検証、TypeScript 型(
z.infer)、OpenAPI 仕様を同時に生成します。手書きの OpenAPI YAML/JSON を重複して作成しないでください。 - すべてのエンドポイントで
createRoute+app.openapi(route, handler)を使う:ハンドラーをapp.get(...)、app.post(...)などで直接登録しないでください。登録すると、そのエンドポイントが/openapi.jsonから消えてしまいます。createRouteの呼び出しでは、responsesに取り得るすべてのレスポンスステータスコードを列挙する必要があります。 - エラーマッピングは共有ヘルパーに集約する:型付きアプリケーションエラーから HTTP へのマッピングは、1 か所で行います(例:
mapApplicationErrorToResponse(context, error))。コントローラー、ユースケース、ルートハンドラー内にreturn context.json({ error, details: { code } }, status)を直接記述しないでください。ヘルパーが新しいエラー型に対応していない場合は、インラインで分岐を追加するのではなく、ヘルパーを拡張してください。 openapi.jsonは生成するものであり、手書きするものではありません:仕様は、登録されたスキーマとルートから@hono/zod-openapiによって生成されます。仕様に誤りがある場合は、仕様ではなく Zod スキーマを修正してください。生成された仕様をコミットしないでください。- Scalar UI は生成された仕様を使用する:インタラクティブなドキュメントは、クライアントが利用するものと同じ
/openapi.jsonから表示されます。別個のドキュメント成果物はありません。 - クリーンアーキテクチャ:コントローラーは、アプリケーションのユースケースを呼び出す薄いアダプターです。ドメインのユースケースとアプリケーション DTO から設計を始め、その後 HTTP にマッピングします。
設計ワークフロー(トップダウン)
- ユースケースから始める
- 境界づけられたコンテキストとユースケース(例:
ListCustomers、CreateOrder)を特定します。 - アプリケーション層で、明確な入出力 DTO を定義します。
- 境界づけられたコンテキストとユースケース(例:
- 選択したスタイルにマッピングする
- スタイルのリファレンスファイルにあるパス、メソッド、パラメーター、レスポンスの規約を適用します。
- ステータスコードとエラーの形式を決定します。
- HTTP とユースケースを相互にマッピングする
- ルート/コントローラーで検証済みの HTTP 入力をアプリケーション DTO に変換し、ユースケースを呼び出して、結果を HTTP に戻します。
- 横断的関心事を一貫して適用する
- 認証、認可、検証、ロギング、冪等性、レート制限、エラー処理には、共有ヘルパーを使用します。
監査ワークフロー
既存のルートまたは API サーフェスを監査する場合は、まずプロジェクトのスタイルを確認してから、レビュー対象の各エンドポイントについて §API Design Checklist を順に確認し、項目ごとに合格/不合格を報告します。確認対象には、メソッドやパスだけでなく、すべてのリクエスト/レスポンススキーマに .openapi("RefName") メタデータが付いているかといった、スキーマレベルの懸念事項も含めます。適切なメソッドと形式を使っていても、Zod スキーマをインラインで記述して(.openapi("RefName") がない)ルートは準拠していないため、必ず指摘してください。プロジェクトで誤ったスタイルを使っているエンドポイントも同様です。
両方向を検証する(リクエストとレスポンス)
createRoute + app.openapi は、リクエスト(パラメーター、クエリ、本文)をランタイムで検証しますが、レスポンスは検証しません。ハンドラーが返す値にかかわらず、Hono はそのまま送信するため、宣言したレスポンススキーマから逸脱する場合があります。responses にレスポンススキーマを宣言し、テストでそれを検証するだけでは、本番環境での保証にはなりません。送信する本文もランタイムで検証し、コントラクトに適合しないレスポンスが気付かれないまま返され、生成された SDK/MCP/CLI ツールを壊す事態を防いでください。
これには、送信前にレスポンススキーマを使ってペイロードをパースする共有レスポンスヘルパーを1つ使います。不一致があると例外が発生し、グローバルエラーハンドラーによって処理されて 500 にマッピングされるため、不正な形式の本文が送信されることはありません。
// src/infra/http/respond.ts — shared, used by every controller in both styles
import type { Context } from "hono";
import type { ContentfulStatusCode } from "hono/utils/http-status";
import type { z } from "@hono/zod-openapi";
export function respond<S extends z.ZodTypeAny>(
context: Context,
schema: S,
payload: z.input<S>,
status: ContentfulStatusCode
) {
return context.json(schema.parse(payload), status); // runtime-validated against the contract
}
schema.parse は非本番環境では常に有効にし、パースのコストが問題になる場合でも、本番環境で少なくともサンプリングして実行してください。レスポンスコントラクトが適用されなくなるため、完全に省略してはいけません。リクエスト側では、OpenAPIHono インスタンスに defaultHook を登録し、リクエスト検証の失敗が Hono のデフォルト本文ではなく、共有のエラー形式(400)になるようにしてください。詳しくは references/openapi-generation.md を参照してください。
コントローラーパターン(両スタイル共通)
コントローラーは app.openapi(route, handler) を使ってルートを自己登録します。createRoute がリクエストを検証するため、別途 zValidator を呼び出す必要はありません。成功時、コントローラーは respond(...) を通じて応答するため、本文は宣言されたレスポンススキーマに照らして検証されます。成功時の形式はスタイルによって異なります(REST ではリソーススキーマを直接返し、POST のみのスタイルでは { data, message } のレスポンススキーマを返します)。エラー処理の経路はどちらも同じで、共有エラーヘルパーを通ります。
import type { OpenAPIHono } from "@hono/zod-openapi";
import { someRoute } from "../routes/some.route";
import { SomeResponseSchema } from "@/api-contracts";
import { mapApplicationErrorToResponse } from "@/infra/http/error-handling";
import { respond } from "@/infra/http/respond";
export class SomeController {
constructor(
private readonly app: OpenAPIHono,
private readonly useCase: SomeUseCase
) {
this.app.openapi(someRoute, async context => {
const input = context.req.valid("json"); // or "query" / "param"
const credential = context.get("credential");
const result = await this.useCase.execute({ ...input, workspaceId: credential.workspaceId });
if (!result.ok) return mapApplicationErrorToResponse(context, result.error);
// REST: respond(context, SomeResourceSchema, result.value, 200)
// POST-only: respond(context, SomeResponseSchema, { data: result.value, message: "Success" }, 200)
return respond(context, SomeResponseSchema, result.value, 200);
});
}
}
成功時の正確な形式と createRoute の具体例については、各スタイルのリファレンスファイルを参照してください。
横断的な考慮事項(両スタイル共通)
これらは説明文だけでなく、コントラクトにも組み込んでください。生成された SDK、MCP、CLI の利用側は、これらを機械可読な情報として必要とします。
- 安全でない操作の冪等性。 作成操作と状態を変更する操作は、安全に再試行できるようにする必要があります。クライアントやゲートウェイはタイムアウト時に再試行するため、単純に再試行すると二重請求や二重作成が発生します。作成/状態変更エンドポイントでは
Idempotency-Key(REST ではヘッダー、POST のみのスタイルでは本文のidempotencyKeyフィールド)を受け付け、重複排除期間中はキーを最初の結果とともに保存してください。同じキーで再度リクエストされた場合は元のレスポンスを返し、同じキーで異なるペイロードが送られた場合は409を返します。読み取りは本質的に冪等であり、キーは不要です。 - バージョン管理と破壊的変更。 最初からサーフェスにバージョンを付けてください(REST では
/v1プレフィックス、POST のみのスタイルではバージョンのセグメントまたはヘッダー)。同じバージョン内では、後方互換性のある追加的な変更のみ行ってください。省略可能なフィールドは追加できますが、既存のフィールドを削除または別の用途に転用したり、既存の入力に対する検証を厳しくしたりしないでください。破壊的変更には、新しいバージョンと非推奨化の手順が必要です。DeprecationとSunsetのレスポンスヘッダーで削除を通知し、移行期間を設けてください。そうしないと、生成された利用側が気付かないまま壊れます。 - レート制限。
429 Too Many Requestsを宣言し、制限を確認できるようにしてください。レスポンスにはRateLimit-Limit、RateLimit-Remaining、RateLimit-Resetを出力し、429ではRetry-Afterも出力します。また、コード生成クライアントがバックオフできるよう、ルートのresponsesヘッダーにもこれらを宣言してください。認証済み API の制限は IP ごとではなく、認証情報/ワークスペースごとに設定してください。
共通規約
認証情報モデル、認可のセマンティクス、フィールド形式は両スタイルに適用され、references/api-conventions.md で詳しく説明しています。コントラクト、認証情報、または金額/日付フィールドを設計する際は、このファイルを参照してください。主なデフォルトは次のとおりです。
- 認証情報には
scopes(何にアクセスできるか)とaccessBoundary(どこにアクセスできるか)を設定します。 広範なアクセスを推測してはなりません。テナント全体へのアクセスには、明示的な境界が必要です。MCP トークンは別種の認証情報です。コード生成/MCP ツールがモデル化できるよう、OpenAPI のsecurityで宣言してください。 - 認可: 範囲外のリソースは
404、存在するがアクセスが禁止されている場合は403を返します。クエリフィルターで認証情報の境界を狭めることはできますが、広げることはできません。 - フィールド形式: JSON フィールド/クエリパラメーターには camelCase を使用します。金額は 10 進文字列と ISO 4217 の組み合わせで表します(浮動小数点数は使用しません)。時点はタイムゾーン付きの ISO 8601、業務日付は
YYYY-MM-DDとします。日時のフィルターでは、対象となる日時の種類を名前に含めます(fromではなくcreatedFrom)。 - 非同期操作:
202 Acceptedと、状態の列挙型を明示したステータスリソースを返し、GETでポーリングします。
ステータスコード
200 OK— 本文を伴う読み取りおよび更新の成功201 Created— 作成の成功。実用上可能であればLocationを含めます202 Accepted— 非同期ジョブの受け付け(REST のリソーススタイル操作)204 No Content— 本文なしでの削除/取り消し/切断の成功304 Not Modified— 表現に変更がない場合の条件付き GET(REST)400 Bad Request— HTTP 境界での不正な入力または検証エラー401 Unauthorized— 認証情報がない、または無効403 Forbidden— 認証済みだが許可されていない404 Not Found— リソースが見つからない409 Conflict— 重複、状態の競合、業務上の不変条件422 Unprocessable Entity— HTTP の検証とは異なるドメイン検証エラー429 Too Many Requests— レート制限500 Internal Server Error— 未処理エラー
エラーエンベロープ(両スタイル共通)
どちらのスタイルでも、エラーには同じ構造化された本文を使用します。errorはUIに表示できる、人が読める文字列です。機械可読な情報はすべてdetailsに格納します。
{
"error": "Human-readable message",
"details": {
"code": "RESOURCE_NOT_FOUND",
"requestId": "req_01HZ..."
}
}
ルール:
error— 常に文字列とし、オブジェクトにしてはいけません。コードを格納してはいけません。details.code— 常に必須です。安定した機械可読形式のSCREAMING_SNAKE_CASEにします。details.requestId— トレーサビリティのため、本番環境では必須です。- 追加の任意のコンテキストは
detailsに格納します。fieldErrors、resourceId、retryAfterなどが該当します。 - 最上位に
codeを置いてはいけません。{ error: { code, message } }のようにネストしてはいけません。自由形式のエラー文字列を返してはいけません。
Zodから生成するOpenAPI仕様
OpenAPI仕様はZodスキーマから生成されるビルド成果物であり、手書きしてはいけません。
Zod schema → .openapi("RefName") → createRoute({ ... })
→ app.openapi(route, handler) → /openapi.json
→ Scalar UI + generated clients
必須ルール:
- 再利用するすべてのスキーマで
.openapi("RefName")を呼び出し、components.schemasに含めます。 - すべてのエンドポイントを
createRouteで宣言し、app.openapi(route, handler)でマウントします。 - エンドポイントが返す可能性のあるすべてのレスポンスステータスコードを
responsesに記載します。 - 認証はOpenAPIの
securityで宣言し、ブートストラップ時にregisterComponent("securitySchemes", ...)で一度だけ登録します。 - 生成されたOpenAPI仕様をコミットしてはいけません。
スキーマの注釈、ブートストラップへの組み込み、クライアントコード生成、よくある間違いを含む全手順については、references/openapi-generation.mdを参照してください。規約に沿ったルート宣言の例については、使用するスタイルのリファレンスファイルを参照してください。
テスト戦略
- コントラクト:有効なペイロードと無効なペイロードを使って、Zodスキーマをユニットテストします。
- コントローラー:Honoルートを統合テストし、ステータスコード、認証、バリデーション、エラーマッピング、および使用するスタイルのレスポンス形式を確認します。
- OpenAPI:サーバーを起動し、テストで
/openapi.jsonを取得して、エンドポイント、メソッド、スキーマ、レスポンスコード、セキュリティを検証します。 - E2E:公開APIまたは生成されたクライアントを通じて、主要なフローを実行します。
API設計チェックリスト
APIの変更をマージする前に、次の点を確認してください。
- エンドポイントがプロジェクトで選択したスタイルに一貫して従っていること(RESTとPOSTのみのスタイルを混在させない)。
- コントラクトが共有パッケージで定義され、
.openapi("RefName")で注釈されていること。 - ルートが
createRouteを使用し、app.openapiでマウントされていること。また、成功時とエラー時のすべてのステータスコード、および正しいsecurityエントリーが宣言されていること。 - パラメーターの配置がスタイルに合っていること(REST:パス/クエリ/ボディ、POSTのみ:JSONボディのみ)。
- レスポンス形式がスタイルに合っていること(REST:リソースを直接返す、POSTのみ:
{ data, message }エンベロープ)。エラーレスポンスには{ error, details: { code } }を使用すること。 - コントローラーが薄く、自己登録を行い、ユースケースに処理を委譲していること。
- 型付きアプリケーションエラーが共有HTTPエラーヘルパーを通じてマッピングされること。
- 認証と認可がOpenAPIで機械可読な形式で記述され、コードで適用されていること。
/openapi.jsonが起動時に再生成され、変更が反映されていること。仕様を取得して検証すること。- テストで成功、バリデーション失敗、認証失敗、認可失敗、ドメイン上の競合をカバーしていること。
- 横断的関心事に対応していること:安全でない操作(作成/状態変更)は
Idempotency-Keyによって冪等にすること。バージョニング方針を、追加のみ/破壊的変更に関するルールとともに明記すること。レート制限ヘッダー(RateLimit-*/Retry-After)を宣言すること。読み取り専用エンドポイントの場合は、冪等性を省略せず、N/Aと記載すること。 - 共有規約に準拠していること(
references/api-conventions.mdを参照):認証情報ではscopesとaccessBoundaryを宣言すること。認可では403と404を正しく使い分け、フィルターによってスコープを拡大しないこと。フィールド形式はcamelCase/金額は10進数文字列+ISO 4217/日時はISO 8601の瞬間/日付はYYYY-MM-DD/時間フィルターは名前付きに従うこと。非同期操作は202とステータスリソースを返すこと。
落とし穴(どちらのスタイルにも共通)
- テーブル名をそのまま流用しないでください。パスが表すのは公開プロダクトのリソースであり、データベーススキーマではありません。
app.get(...)/app.post(...)で OpenAPI のルート登録を迂回しないでください。- 認証要件を自由記述の
descriptionテキストに隠さず、securityを使用してください。 - バージョン管理せずに、互換性を破るレスポンス変更を行わないでください。
- エラーレスポンスをインラインで記述せず、共有ヘルパーを通してください。
スタイル固有の命名規則と落とし穴は、各スタイルのリファレンスファイルに記載されています。