Hono実装ガイド — Zodバリデーション・OpenAPI・JWT認証を組み合わせた本番品質API構築【2026年版】
HonoでZodバリデーション・JWT認証・OpenAPI自動生成を統合した本番品質APIの実装方法を体系的に解説。CORS、レート制限、エラーハンドリング、ロギング、Swagger UI統合まで網羅。
本番品質APIとは何か? HonoでZod・JWT・OpenAPIを統合する方法
本番品質APIに最低限必要な要素は「バリデーション・認証・OpenAPI仕様・エラーハンドリング・ロギング・レート制限」の6つです。Honoはこれらをすべて軽量かつ型安全に実装できるフレームワークです。本記事では各要素の実装方法を体系的に解説します。
本番APIアーキテクチャ全体像
Zodバリデーション — @hono/zod-validatorの使い方
`@hono/zod-validator`を使うと、リクエストのJSON・クエリパラメータ・ヘッダー・Cookieなどを型安全に検証できます。バリデーション失敗時は自動的に400エラーを返し、成功時は`c.req.valid()`で型付きデータにアクセスできます。
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const app = new Hono()
const userSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(0).max(150)
})
app.post('/users', zValidator('json', userSchema), (c) => {
const user = c.req.valid('json') // 型: { name: string; email: string; age: number }
return c.json({ id: 1, ...user }, 201)
})バリデーション対象一覧
Honoでバリデーション可能なリクエスト要素は以下の通りです。
| ターゲット | 第1引数 | 取得方法 |
|---|---|---|
| JSONボディ | `'json'` | `c.req.valid('json')` |
| フォームデータ | `'form'` | `c.req.valid('form')` |
| クエリパラメータ | `'query'` | `c.req.valid('query')` |
| パスパラメータ | `'param'` | `c.req.valid('param')` |
| HTTPヘッダー | `'header'` | `c.req.valid('header')` |
| Cookie | `'cookie'` | `c.req.valid('cookie')` |
バリデーション失敗時のカスタムエラーレスポンス
app.post(
'/users',
zValidator('json', userSchema, (result, c) => {
if (!result.success) {
return c.json(
{
error: 'Validation failed',
details: result.error.flatten()
},
422
)
}
}),
(c) => {
const user = c.req.valid('json')
return c.json({ id: 1, ...user }, 201)
}
)JWT認証ミドルウェアの実装
Honoには`hono/jwt`が標準で含まれており、追加パッケージなしでJWT認証を実装できます。`jwt()`ミドルウェアを特定パスに適用し、検証済みペイロードは`c.get('jwtPayload')`で取得します。
import { Hono } from 'hono'
import { jwt } from 'hono/jwt'
const app = new Hono()
// /auth/* 以下のルートにJWT認証を適用
app.use('/auth/*', jwt({
secret: process.env.JWT_SECRET!,
alg: 'HS256'
}))
app.get('/auth/me', (c) => {
const payload = c.get('jwtPayload')
return c.json(payload)
})JWTトークン発行(ログインエンドポイント)
import { sign } from 'hono/jwt'
app.post('/login', zValidator('json', z.object({
email: z.string().email(),
password: z.string().min(8)
})), async (c) => {
const { email, password } = c.req.valid('json')
// DB照合(擬似コード)
const user = await db.findUserByEmail(email)
if (!user || !(await verifyPassword(password, user.passwordHash))) {
return c.json({ error: 'Invalid credentials' }, 401)
}
const token = await sign(
{
sub: user.id,
email: user.email,
exp: Math.floor(Date.now() / 1000) + 60 * 60 // 1時間
},
process.env.JWT_SECRET!,
'HS256'
)
return c.json({ token })
})JWT対応アルゴリズム一覧
Honoが対応するJWT署名アルゴリズムは以下の通りです。
| アルゴリズム | 種別 | 推奨用途 |
|---|---|---|
| HS256 | HMAC | 内部API、シンプルな認証 |
| HS384 | HMAC | HS256より高セキュリティ |
| HS512 | HMAC | 最高強度HMAC |
| RS256 | RSA | 公開鍵配布が必要な場合 |
| RS384 | RSA | RS256より高セキュリティ |
| RS512 | RSA | 最高強度RSA |
| ES256 | ECDSA | 短い鍵で高セキュリティ |
| EdDSA | Ed25519 | 最新推奨、高速・小サイズ |
その他の認証方式 — Bearer Auth・Basic Auth
JWTのほかに、HonoはBearer AuthとBasic Authも標準サポートしています。
| 認証方式 | インポート | 主な用途 |
|---|---|---|
| JWT | `hono/jwt` | ユーザー認証、SPA/モバイル |
| Bearer Auth | `hono/bearer-auth` | API Keyによる認証 |
| Basic Auth | `hono/basic-auth` | 管理画面、開発環境 |
import { bearerAuth } from 'hono/bearer-auth'
import { basicAuth } from 'hono/basic-auth'
// Bearer Auth(APIキー)
app.use('/api/*', bearerAuth({ token: process.env.API_KEY! }))
// Basic Auth
app.use('/admin/*', basicAuth({
username: 'admin',
password: process.env.ADMIN_PASSWORD!
}))@hono/zod-openapi — OpenAPI仕様の自動生成
`@hono/zod-openapi`を使うと、Zodスキーマから直接OpenAPI 3.0仕様を自動生成できます。`OpenAPIHono`クラスと`createRoute()`を組み合わせることで、コードとドキュメントを常に同期した状態に保てます。
import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi'
const app = new OpenAPIHono()
const UserCreateSchema = z.object({
name: z.string().min(1).openapi({ example: 'Taro Yamada' }),
email: z.string().email().openapi({ example: 'taro@example.com' })
})
const UserResponseSchema = z.object({
id: z.number().openapi({ example: 1 }),
name: z.string(),
email: z.string()
})
const createUserRoute = createRoute({
method: 'post',
path: '/users',
tags: ['Users'],
request: {
body: {
content: {
'application/json': { schema: UserCreateSchema }
}
}
},
responses: {
201: {
content: { 'application/json': { schema: UserResponseSchema } },
description: 'User created successfully'
},
422: {
description: 'Validation error'
}
}
})
app.openapi(createUserRoute, (c) => {
const { name, email } = c.req.valid('json')
return c.json({ id: 1, name, email }, 201)
})
// OpenAPI仕様JSONエンドポイント
app.doc('/openapi.json', {
openapi: '3.0.0',
info: { title: 'My Production API', version: '1.0.0' }
})Swagger UI統合 — @hono/swagger-ui
import { swaggerUI } from '@hono/swagger-ui'
// Swagger UIを /ui で提供
app.get('/ui', swaggerUI({ url: '/openapi.json' }))
// 開発環境のみUIを有効化する例
if (process.env.NODE_ENV !== 'production') {
app.get('/ui', swaggerUI({ url: '/openapi.json' }))
}CORS・レート制限・ロギングの設定
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { rateLimiter } from 'hono-rate-limiter'
const app = new Hono()
// ロギング
app.use('*', logger())
// CORS設定
app.use('/api/*', cors({
origin: ['https://example.com', 'https://app.example.com'],
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
allowHeaders: ['Authorization', 'Content-Type'],
maxAge: 86400
}))
// レート制限(1分間に60リクエスト)
app.use('/api/*', rateLimiter({
windowMs: 60 * 1000,
limit: 60,
keyGenerator: (c) => c.req.header('x-forwarded-for') ?? 'unknown'
}))グローバルエラーハンドリング — HTTPException
import { HTTPException } from 'hono/http-exception'
// カスタムエラーをスロー
app.get('/resource/:id', async (c) => {
const resource = await db.find(c.req.param('id'))
if (!resource) {
throw new HTTPException(404, { message: 'Resource not found' })
}
return c.json(resource)
})
// グローバルエラーハンドラー
app.onError((err, c) => {
if (err instanceof HTTPException) {
return c.json(
{ error: err.message, status: err.status },
err.status
)
}
console.error('Unexpected error:', err)
return c.json({ error: 'Internal Server Error' }, 500)
})本番デプロイ前チェックリスト
本番環境へデプロイする前に以下の項目をすべて確認してください。
| カテゴリ | チェック項目 | 優先度 |
|---|---|---|
| セキュリティ | JWT_SECRETが十分な長さ(32文字以上)か | 必須 |
| セキュリティ | CORS originが本番URLのみに制限されているか | 必須 |
| セキュリティ | 機密情報が環境変数で管理されているか | 必須 |
| バリデーション | 全エンドポイントにzValidatorが適用されているか | 必須 |
| エラー処理 | onErrorハンドラーが設定されているか | 必須 |
| エラー処理 | スタックトレースが本番レスポンスに含まれていないか | 必須 |
| パフォーマンス | レート制限が適切に設定されているか | 推奨 |
| パフォーマンス | 不要なミドルウェアが無効化されているか | 推奨 |
| ドキュメント | OpenAPI仕様が最新の状態か | 推奨 |
| ドキュメント | Swagger UIが本番環境で無効化されているか | 推奨 |
| テスト | 主要エンドポイントの統合テストが通過しているか | 必須 |
| ロギング | 構造化ログが出力されているか | 推奨 |
よくある質問(FAQ)
Q1. `@hono/zod-validator`と`@hono/zod-openapi`の違いは? `@hono/zod-validator`はバリデーションのみを担当する軽量パッケージです。`@hono/zod-openapi`はバリデーションに加えてOpenAPI仕様の自動生成機能を含む上位互換パッケージです。OpenAPI仕様が不要な場合は前者の方がバンドルサイズを小さく保てます。 Q2. JWTのリフレッシュトークンはどう実装する? アクセストークン(有効期限15〜60分)とリフレッシュトークン(有効期限7〜30日)を分けて発行し、リフレッシュトークンはデータベースに保存します。`/auth/refresh`エンドポイントでリフレッシュトークンを検証後、新しいアクセストークンを発行するのが標準的なパターンです。 Q3. 複数ルートに異なるJWT設定を適用できますか? はい。`app.use('/admin/*', jwt({ secret: ADMIN_SECRET }))` と `app.use('/api/*', jwt({ secret: API_SECRET }))` のように、パスごとに異なる設定を適用できます。 Q4. Zodでネストされたオブジェクトをバリデーションする方法は? `z.object()`をネストするだけです。例: `z.object({ address: z.object({ city: z.string(), zip: z.string().regex(/^\d{7}$/) }) })` のように記述します。 Q5. レート制限をユーザーIDごとに適用できますか? `keyGenerator`関数をカスタマイズすることで可能です。認証後のルートでは `keyGenerator: (c) => c.get('jwtPayload').sub` のようにJWTペイロードのユーザーIDをキーとして使用できます。 Q6. OpenAPI仕様をTypeScriptの型として利用できますか? `@hono/zod-openapi`では`createRoute()`の戻り値から型を推論できます。また`openapi-typescript`などのツールでOpenAPI JSONから型定義を自動生成することも可能です。 Q7. Honoのテストはどう書く? `@hono/testing`(または`hono/testing`)の`testClient()`を使うと型安全なHTTPクライアントでテストできます。VitestやJestと組み合わせて、実際のHTTPサーバーを起動せずにハンドラーを直接テストできます。
Oflightの技術支援サービス
Honoを活用した本番品質APIの設計・開発・運用保守を支援しています。バリデーション設計、認証基盤構築、OpenAPI仕様の整備、CloudflareWorkers/Vercel Edgeへのデプロイ最適化まで、トータルでサポートします。詳しくはソフトウェア開発サービスをご覧ください。
お気軽にご相談ください
お問い合わせ