🧪 @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の正しい使い所について」と合わせて読むことでより一層理解が深まると思うので、こちらもご一読いただけると幸いです。
最後になりますが、スパイダープラスでは一緒に働ける仲間を募集しております。
スパイダープラスに少しでも興味を持たれた方は是非、カジュアル面談などでお話をしてみませんか?ご連絡をお待ちしております。