はじめに
前回に引き続き、Hono関連の記事を書きました。
Honoでは OpenAPI 定義と API 実装を同じ場所で管理できます。
さらに Swagger UI を組み合わせることで、ブラウザから API ドキュメントの閲覧や動作確認も可能になります。
この記事では Hono + Zod OpenAPI + Swagger UI を使った最小構成を紹介します。
本文に入る前に、各ツールの役割を整理しておきます。
- OpenAPI → API仕様を記述するための標準フォーマット
- Swagger UI → OpenAPI仕様書をブラウザで閲覧・実行できるツール
- Zod OpenAPI → ZodスキーマからOpenAPI仕様書を生成する仕組み
検証環境
- Node.js: v20.x
- パッケージマネージャー: npm
セットアップ
まずはベースとなるHonoのプロジェクトを作成します。
npm create hono@latest hono-openapi-demo
今回はランタイムにNode.jsを使うため、テンプレートはnodejsを指定します。
✔ Using target directory … hono-openapi-demo
✔ Which template do you want to use? nodejs
✔ Do you want to install project dependencies? Yes
✔ Which package manager do you want to use? npm
✔ Cloning the template
✔ Installing project dependencies
🎉 Copied project files
Get started with: cd hono-openapi-demo
次に、今回使用するZod OpenAPIとSwagger UIのミドルウェアをインストールします。
npm install @hono/swagger-ui @hono/zod-openapi
次の章から、まずは最もシンプルな方法でSwagger UIを表示させ、その後にZod OpenAPIを使って実装とドキュメントを連動させる形に拡張します。
Swagger UIの導入
まずはZod OpenAPIを使わず、HonoにSwagger UIのミドルウェアだけを組み込んでみます。書籍管理アプリのAPIを想定したサンプルです。
src/index.ts の内容を以下のように書き換えます。
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import { swaggerUI } from '@hono/swagger-ui'
const app = new Hono()
// 手動で定義したAPI仕様(JSON)を返すエンドポイント
app.get('/doc', (c) => {
return c.json({
openapi: '3.0.0',
info: {
title: 'Book API Sample',
version: '1.0.0',
description: 'Honoで作成した書籍管理APIのサンプル',
},
paths: {
'/books/{id}': {
get: {
summary: '書籍情報の取得',
description: '指定されたIDに一致する書籍の情報を取得します。',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: {
type: 'string',
},
description: '書籍の識別ID',
},
],
responses: {
200: {
description: '成功',
},
},
},
},
},
})
})
// Swagger UIの表示用エンドポイント
app.get('/ui', swaggerUI({ url: '/doc' }))
serve({
fetch: app.fetch,
port: 3000
}, (info) => {
console.log(`Server is running on http://localhost:${info.port}`)
})
ここで一度、npm run devでサーバーを起動し、http://localhost:3000/uiにアクセスしてみます。このように、Swagger UIのドキュメントを確認することができます。
ここではあくまでドキュメントを生成しただけで、実際の/books/{id}エンドポイントができたわけではありません。そこへ実際にリクエストを送信しても、Hono側に処理が書かれていないため404エラーが返ってきます。
また、このままではAPI実装を追加・変更しても、その度にOpenAPI仕様(JSON)をすべて手書き修正することになります。
そこで次にZod OpenAPIを導入します。
Zod OpenAPIで拡張する
手書きしていたAPI仕様を、Zodのスキーマから自動生成できるように拡張します。
-
通常版のHonoクラスからOpenAPIHonoクラスに切り替える
-
zオブジェクトを使ってスキーマを定義
-
ルート(
/books/{id})の定義
import { serve } from '@hono/node-server'
import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi'
import { swaggerUI } from '@hono/swagger-ui'
const app = new OpenAPIHono()
// パラメータのスキーマを定義
const ParamsSchema = z.object({
id: z.string().openapi({
param: { name: 'id', in: 'path' },
example: '123',
description: '書籍の識別ID',
}),
})
// レスポンスのスキーマを定義
const BookSchema = z
.object({
id: z.string().openapi({ example: '123', description: '書籍ID' }),
title: z
.string()
.openapi({ example: 'Hono入門', description: '書籍のタイトル' }),
author: z.string().openapi({ example: '山田太郎', description: '著者名' }),
publishedAt: z
.string()
.openapi({ example: '2026-01-01', description: '出版日' }),
})
.openapi('Book')
// 404エラー時のレスポンススキーマ
const ErrorSchema = z.object({
message: z.string().openapi({ example: 'Not Found' }),
})
// ルート(API仕様)の定義
const getBookRoute = createRoute({
method: 'get',
path: '/books/{id}',
summary: '書籍情報の取得',
description: '指定されたIDに一致する書籍の情報を取得します。',
request: {
params: ParamsSchema,
},
responses: {
200: {
content: { 'application/json': { schema: BookSchema } },
description: '指定した書籍の情報を返します',
},
404: {
content: { 'application/json': { schema: ErrorSchema } },
description: '書籍が見つかりませんでした',
},
},
})
const dummyBooks = [
{
id: '123',
title: 'Hono入門',
author: '山田太郎',
publishedAt: '2026-01-01',
},
{
id: '456',
title: 'HonoによるWeb API開発',
author: '佐藤次郎',
publishedAt: '2026-05-15',
},
]
// ルート定義とハンドラーの紐付け
app.openapi(getBookRoute, (c) => {
const { id } = c.req.valid('param')
const book = dummyBooks.find((b) => b.id === id)
if (!book) {
return c.json({ message: 'Not Found' }, 404)
}
return c.json(book, 200)
})
app.doc('/doc', {
openapi: '3.0.0',
info: {
title: 'Book API Sample',
version: '1.0.0',
description:
'HonoとZod OpenAPIで自動生成した書籍管理APIのドキュメントです。',
},
})
app.get('/ui', swaggerUI({ url: '/doc' }))
serve(
{
fetch: app.fetch,
port: 3000,
},
(info) => {
console.log(`Server is running on http://localhost:${info.port}`)
},
)
これで自動的にスキーマまで反映された仕様書が完成します。再度ブラウザでhttp://localhost:3000/uiにアクセスします。
今度はZodで定義した通りのデータ構造や、各項目のdescription、データのexampleが自動的にSwagger UIへ反映されていることが分かります。
今回はドキュメントだけでなく実際のAPI処理も紐づいているため、Try it out から id に 123 を入力して実行すれば、実際にダミーデータが返ってきます。
この構成にすることで、APIのバリデーション、実際の処理、APIドキュメントの3つが常に一箇所で管理され、仕様書が古くなる問題を防ぐことができます。
参考

