
🧪 @hono/zod-openapi でAPI開発をネクストレベルに
前回投稿の記事、「TypeScriptとhonoで作るDI実践入門」ではHonoとTypescriptで型安全なDIを実践しました。
しかし、前回の実装では以下の課題がありました。
今回はこの課題に対して、@hono/zod-openapiを導入し解決していきます。
@hono/zod-openapiを導入することで、ZodスキーマからOpenAPIドキュメントを自動生成し、リクエスト/レスポンスのバリデーション、そしてルートハンドラーのコンテキストの型安全性を更に向上させることができます。
📋 @hono/zod-openapi 導入ステップバイステップ
ここからは、既存のアプリケーションに @hono/zod-openapi を組み込み、より洗練されたAPIへと進化させる手順を見ていきましょう。
前回投稿の記事、TypeScriptとHonoで作るDI実践入門の続きとなりますので、まだ、ご覧なってない方はこちらをご確認いただくとスムーズだと思います。
1. 依存関係の追加
まずは、必要なパッケージをインストールします。
Zodはスキーマ定義・バリデーションライブラリです。
pnpm add @hono/zod-openapi zod zod-openapi
2. スキーマを定義
@hono/zod-openapi の中核となるのがZodスキーマです。
これを使って、リクエストのパラメータやレスポンスのデータ構造を厳密に定義します。
// src/schemas/UserSchema.ts
import { z } from '@hono/zod-openapi';
// Userエンティティのスキーマ定義
export const UserSchema = z.object({
id: z.number().openapi({ example: 1, description: 'ユーザーID' }), // exampleを追加するとドキュメントが見やすくなります
name: z.string().openapi({ example: 'Alice', description: 'ユーザー名' }),
}).openapi({ // スキーマ自体にもメタ情報を付与する
title: 'User',
description: 'A user entity which represents a user in the system.',
});
// リクエストパラメータのスキーマ定義 (URLのパスパラメータ用)
export const ParamsSchema = z.object({
id: z.string() // Honoのパスパラメータはデフォルトでstring
.regex(/^\d+$/, { message: 'ID must be a numeric string' }) // 正規表現で数値文字列であることを保証
.openapi({
param: {
name: 'id',
in: 'path', // パスパラメータであることを明示
},
example: '1',
description: '取得するユーザーのID',
}),
});
zod-openapi/extend をインポートすることで、z.number().openapi({...}) のように、各スキーマフィールドやスキーマ全体にOpenAPIドキュメント用の情報を付加できるようになります。
example や description を記述しておくと、生成されるドキュメントが非常にリッチになります。 ParamsSchema では、パスパラメータ id が数値文字列であることまでバリデーションしています。
これで、ハンドラー内で id が不正なフォーマットであるケースを早期に排除できます。
3. OpenAPIルートの定義
次に、先ほど定義したZodスキーマを使って、OpenAPIと連携する新しいルート定義を作成します。
ファイル名を userRoute.ts とし、ルーティング定義とDIミドルウェアの適用をここにまとめましょう。
// src/routes/userRoute.ts
import { createRoute } from '@hono/zod-openapi';
import { ParamsSchema, UserSchema } from '../schemas/UserSchema'; // 作成したZodスキーマ
import { userServiceDep } from '../dependencies/userServiceDependency';
import { userRepoDep } from '../dependencies/userRepositoryDependency'; // userRepositoryも使う場合は追加
// OpenAPIと連携するルートを作成
export const getUserRoute = createRoute({
method: 'get', // HTTPメソッド
path: '/users/{id}', // パス (プレースホルダもそのまま)
// このルートで利用するミドルウェアを配列で指定
middleware: [
userRepoDep.middleware('userRepository'), // UserRepositoryも必要なら注入
userServiceDep.middleware('userService'), // UserServiceを注入
] as const, // as constにより型推論が強化される
request: {
// リクエストパラメータのスキーマを指定
params: ParamsSchema,
},
responses: {
// 200 OK時のレスポンス定義
200: {
description: 'User retrieved successfully. Returns user data.',
content: {
'application/json': {
schema: UserSchema, // UserSchemaがレスポンスの形式であることを示す
},
},
},
// 400 Bad Request時のレスポンス定義 (例)
400: {
description: 'Invalid request parameters.',
content: {
'application/json': {
schema: z.object({ error: z.string() }).openapi({title: 'ErrorResponse'}),
}
}
},
// 404 Not Found時のレスポンス定義 (例)
404: {
description: 'User not found.',
content: {
'application/json': {
schema: z.object({ error: z.string() }).openapi({title: 'ErrorResponse'}),
}
}
}
},
// タグやサマリーも追加してドキュメントを分かりやすく
tags: ['Users'],
summary: 'Get a single user by ID',
});
createRoute 関数は HTTPメソッド、パス、リクエスト/レスポンスのスキーマ、ミドルウェアまで、ルートに関する情報を一元的に定義できます。
特に注目すべきは middleware: [ ... ] as const です。
as const を付けることで、ミドルウェアの配列の型がリテラル型として推論され、後述するハンドラーでの c.var の型安全性が飛躍的に向上します。
hono-simple-di のミドルウェアもここで指定することで、このルート専用のDIコンテキストが作られるイメージです。
responses に各ステータスコードごとのレスポンススキーマを定義することで、生成されるOpenAPIドキュメントが非常に詳細になり、クライアント開発者との連携もスムーズになります。
4. 型安全なハンドラーの実装
先ほど定義した getUserRoute の型情報をフル活用した、完全に型安全なハンドラーを実装しましょう。 ファイル名を fetchUserHandler.ts とします。
// src/handler/fetchUserHandler.ts
import { RouteHandler } from "@hono/zod-openapi"; // RouteHandler型をインポート
import { getUserRoute } from "../routes/userRoute"; // 作成したOpenAPIルート定義
// import { User } from "../interfaces/User"; // 必要に応じて
// getUserRouteの型情報を元に、厳密に型付けされたハンドラーを作成
const handler: RouteHandler = async (c) => {
// c.req.valid('param') で、型安全にバリデーション済みパラメータを取得!
// ParamsSchemaで定義した型 (ここでは { id: string }) で取得できる
const { id } = c.req.valid('param');
// c.var.userService も、getUserRouteのmiddleware定義に基づいて型付けされる!
const user = c.var.userService.getUserById(Number(id));
if (!user) {
// Zodスキーマに沿ったエラーレスポンスを返す (この例では省略していますが、本来はスキーマに合わせたオブジェクトを生成)
return c.json({ error: 'User not found' }, 404);
}
// レスポンスも UserSchema に基づいて型チェックされる (return c.json(user) の user が UserSchema に合致するか)
return c.json(user);
};
export default handler;
RouteHandler
- c.req.valid('param'):もともと、c.req.param('id') の戻り値が string | undefined でしたが、今回 Zodによるバリデーション済みのパスパラメータが、ParamsSchema で定義した通りの型で取得できます。
不正なリクエストは、このハンドラーに到達する前にエラーレスポンスが返されます。 - c.var.userService: getUserRoute の middleware で指定した依存関係 (userService) が、UserService 型として完璧に型推論されます。
以前 src/types/hono.d.ts で行った ContextVariableMap の拡張は、@hono/zod-openapi を使うルートでは不要になるケースが多いです(グローバルに使いたい場合は依然有効)。
この対応で型の恩恵を最大限に受けながら、ビジネスロジックに集中できます。
↓従前までの内容: 対応前はcの型が推論されず、any型となっていた

↓今回の対応: cの型が推論され見通しが良くなった

5. OpenAPIHonoアプリケーションの設定
最後に、index.ts (または app.ts を修正して index.ts から呼び出す形でもOK) を変更します。
Hono の代わりに OpenAPIHono を使用し、定義したルートとハンドラーを登録します。
// src/index.ts (または app.ts を修正してこちらをエントリーポイントに)
import { serve } from '@hono/node-server';
import { OpenAPIHono } from '@hono/zod-openapi'; // OpenAPIHonoをインポート
import { getUserRoute } from './routes/userRoute'; // OpenAPIルート定義
import fetchUserHandler from './handler/fetchUserHandler'; // 型安全ハンドラー
// OpenAPIHonoインスタンスを作成
const app = new OpenAPIHono();
// 通常のHonoと同様にルートを定義することも可能
app.get('/', (c) => {
return c.json({
message: 'Hono DI Application with OpenAPI へようこそ!',
endpoints: {
users: '/users/{id}',
docs: '/doc' // OpenAPIドキュメントのエンドポイント
}
});
});
// OpenAPI用のユーザールートを登録
// 第1引数にルート定義、第2引数にハンドラーを渡す
app.openapi(getUserRoute, fetchUserHandler);
// OpenAPI ドキュメント (Swagger UI) のエンドポイントを追加
app.doc('/doc', {
openapi: '3.0.0', // OpenAPIのバージョン
info: {
version: '1.0.0',
title: 'My Super User API', // APIのタイトル
description: 'An API to manage users, built with Hono and OpenAPI.', // APIの説明
},
// servers: [{ url: 'http://localhost:3000', description: 'Development server' }] // サーバー情報も追加可能
});
const port = 3000;
console.log(`Server is running on http://localhost:${port}`);
console.log(`OpenAPI docs available at http://localhost:${port}/doc`); // Swagger UIへの案内!
serve({
fetch: app.fetch,
port
});
OpenAPIHono クラスを使用し、app.openapi(getUserRoute、 fetchUserHandler) のようにして、定義済みのルートとハンドラーを結びつけます。
そして、app.doc('/doc', {...}) で、Swagger UI形式のAPIドキュメントを /doc エンドポイントで公開することができます。
これで、再度 pnpm dev でサーバーを起動し、http://localhost:3000/doc にアクセスしてみてください。
美しいSwagger UIが表示され、定義したスキーマ情報が反映されたAPIドキュメントが自動生成されているはずです。
もちろん、/users/1 などのエンドポイントも以前と同様に動作します。
🎯 @hono/zod-openapi 導入によるメリット・デメリット
@hono/zod-openapi を導入する価値を改めて整理しましょう。
- 型安全性:
- c.req.valid('param') による、バリデーション済みかつ正確に型付けされたリクエストデータの取得。
- ルート定義に紐付いたミドルウェア (c.var) の型推論が強化され、手動メンテナンス箇所が減少。
- レスポンスの型もZodスキーマで定義できるため、意図しないデータ構造のレスポンスを防ぎ、APIの信頼性が向上する。
- 自動ドキュメント生成:
- /doc エンドポイントにアクセスするだけで、リッチなSwagger UIが利用可能。APIの仕様が常に最新のコードと同期します。
- 強力かつ柔軟なバリデーション:
- Zodの豊富なバリデーションルール(文字列、数値、日付、オブジェクト、配列、etc.)カスタムバリデーションを駆使して、堅牢な入力チェックができる。
- エラーメッセージも細かくカスタマイズ可能。
- 開発効率の向上:
- OpenAPI仕様準拠でAPIの標準化:
一方、@hono/zod-openapi導入の潜在的なデメリット・注意点についても解説します。
- 学習コスト:
- コード記述量の増加
🎉 まとめ
本記事では、HonoとTypeScriptをベースに、@hono/zod-openapiを活用した型安全でドキュメント化されたAPI開発を、ハンズオン形式で解説しました。
@hono/zod-openapiの導入は、小規模なアプリケーションではやや冗長に映るかもしれません。
しかし、TypeScriptや hono-simple-diと組み合わせることで得られる、厳格な型安全性やAPIドキュメントの自動生成といったメリットは、その手間を上回る価値があると思います。
これにより、アプリケーションのメンテナンス性は大幅に向上し、開発体験もより快適なものになるはずです。ぜひ本ガイドを参考に、このパワフルな開発手法を体験してみてください。
また、「TypeScriptとHonoで作るDI実践入門」と「ソフトウェアアーキテクチャの本質と依存関係の整理、あるいはinterfaceの正しい使い所について」と合わせて読むことでより一層理解が深まると思うので、こちらもご一読いただけると幸いです。
最後になりますが、スパイダープラスでは一緒に働ける仲間を募集しております。
スパイダープラスに少しでも興味を持たれた方は是非、カジュアル面談などでお話をしてみませんか?ご連絡をお待ちしております。