Honoには公式のZodバリデーターミドルウェアが用意されており、これを使うだけで「バリデーション」と「TypeScriptの型推論」を一箇所に集約できます。導入からカスタムエラーハンドリングまでを試してみました。
パッケージの導入
まずは必要なパッケージをインストールします。今回はNode.js環境を前提としますが、BunやDenoでも同様に動作します。
npm install hono @hono/zod-validator zod @hono/node-server
-
hono: Webフレームワーク本体 -
zod: スキーマ定義・バリデーションライブラリ -
@hono/zod-validator: Hono公式が提供するZodインテグレーション用ミドルウェア -
@hono/node-server: Node.js上でHonoを起動するためのサーバーアダプター
QueryとBodyをZodで縛る
リクエストの「Queryパラメータ(GET)」と「Body(POST)」をZodスキーマで検証します。
スキーマの定義
検証ルールとなるZodスキーマを作成します。
プロジェクトのルートディレクトリに schemas.ts を新規作成し、以下の定義を記述します。
import { z } from 'zod';
// 1. クエリパラメータ用のスキーマ (例: ページネーション)
// 空文字や未指定時にも安全に数値へ変換するため、z.coerceを使用します
export const paginationQuerySchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().positive().max(100).default(10),
});
// 2. リクエストボディ用のスキーマ (例: ユーザー作成)
export const createUserSchema = z.object({
name: z.string().min(2, '名前は2文字以上で入力してください'),
email: z.string().email('無効なメールアドレス形式です'),
age: z.number().min(18, '18歳以上である必要があります').optional(),
});
Honoのルートに適用する
作成したスキーマを、Hono公式の zValidator ミドルウェアを使ってルーティングに挟み込みます。
プロジェクトのルートディレクトリに、メインファイルとなる index.ts を新規作成し、以下のコードを記述します。
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { serve } from '@hono/node-server';
import { paginationQuerySchema, createUserSchema } from './schemas';
const app = new Hono();
// GETリクエストのQueryパラメータを検証
app.get('/users', zValidator('query', paginationQuerySchema), (c) => {
// ここで型が完全に自動推論される
const { page, limit } = c.req.valid('query');
// pageとlimitは自動的にnumber型として扱えます
return c.json({
message: `Users list (Page: ${page}, Limit: ${limit})`
});
});
// POSTリクエストのBodyを検証
app.post('/users', zValidator('json', createUserSchema), (c) => {
// ここでもBodyの中身は型安全になります
const validatedData = c.req.valid('json');
return c.json({
message: 'User created successfully!',
user: validatedData // name, email, age (型推論あり)
}, 201);
});
// Node.js環境でサーバーを起動するための設定
serve({
fetch: app.fetch,
port: 3000
}, (info) => {
console.log(`Server is running on http://localhost:${info.port}`);
});
export default app;
動作確認
作成したコードの動作確認を行います。Node.js環境であれば、開発サーバーを動かすために tsx などの実行ツールを使用してサーバーを起動します。
コマンドラインで以下のコマンドを実行し、サーバーを起動させておきます。
npx tsx --watch index.ts
※ アプリケーションの実行ポートや起動処理は、使用しているランタイム(Node.jsのHonoアダプターなど)に合わせて適切に読み替えてください。以下では、ローカルのポート3000でサーバーが起動している前提で解説します。
1. Queryパラメータ(GET)の検証
別のターミナルを開き、curlコマンドでGETリクエストを送信して挙動を確認します。
正常なリクエスト(数値を指定した場合):
curl "http://localhost:3000/users?page=2&limit=20"
レスポレス:
{"message":"Users list (Page: 2, Limit: 20)"}
pageとlimitがスキーマ側で正常に数値(Number型)へ変換されていることが確認できます。
デフォルト値の検証(パラメータを省略した場合):
curl "http://localhost:3000/users"
レスポンス:
{"message":"Users list (Page: 1, Limit: 10)"}
スキーマに定義したデフォルト値(page=1, limit=10)が適用されています。
2. Body(POST)の検証
正常なデータを送信して、POSTリクエストの挙動を確認します。
正常なリクエスト:
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name": "hono", "email": "test@example.com", "age": 20}'
レスポンス:
{"message":"User created successfully!","user":{"name":"hono","email":"test@example.com","age":20}}
異常なデータを送信して、デフォルトのバリデーションエラーの挙動を確認します。
異常なリクエスト(emailの形式が不正):
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name": "hono", "email": "invalid-email", "age": 20}'
レスポンス:
{
"success": false,
"error": {
"name": "ZodError",
"message": "[\n {\n \"origin\": \"string\",\n \"code\": \"invalid_format\",\n \"format\": \"email\",\n \"path\": [\n \"email\"\n ],\n \"message\": \"無効なメールアドレス形式です\"\n }\n]"
}
}
デフォルトの挙動では、このように詳細な ZodError の情報を含んだJSONと、400ステータスコードが返却されます。
カスタムエラーハンドリング
zValidator は第3引数にフック関数を受け取ることができ、ここでバリデーション失敗時の挙動をカスタムできます。その際、後続のハンドラーでの型推論を壊さないために、c.json()などのレスポンスオブジェクトを returnして処理を終了させる記述方法をとります。
先ほど作成した index.ts の内容を以下のように修正し、カスタムエラーハンドリングを追加します。
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { serve } from '@hono/node-server';
import { createUserSchema } from './schemas';
const app = new Hono();
app.post(
'/users',
zValidator('json', createUserSchema, (result, c) => {
// バリデーションに失敗した場合
if (!result.success) {
// Zodのエラー(issues配列)から、フロントエンドで扱いやすいフラットなエラーオブジェクトを生成
const formattedErrors = result.error.issues.map((issue) => ({
field: issue.path.join('.'),
message: issue.message,
}));
return c.json(
{
success: false,
message: '入力値にエラーがあります',
errors: formattedErrors,
},
400 // Bad Request
);
}
}),
(c) => {
// フック関数の戻り値に return c.json() を記述したことで、ここの data も正しく型推論されます
const data = c.req.valid('json');
return c.json({ success: true, data });
}
);
serve({
fetch: app.fetch,
port: 3000
}, (info) => {
console.log(`Server is running on http://localhost:${info.port}`);
});
export default app;
動作確認
サーバーを再起動した状態で、再度エラーになるリクエストを送信してレスポンスの変化を確認します。
検証用リクエスト(名前が短く、emailが不正、かつ年齢が18歳未満):
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name": "A", "email": "not-an-email", "age": 16}'
実際のレスポンス(エラー時):
{
"success": false,
"message": "入力値にエラーがあります",
"errors": [
{ "field": "name", "message": "名前は2文字以上で入力してください" },
{ "field": "email", "message": "無効なメールアドレス形式です" },
{ "field": "age", "message": "18歳以上である必要があります" }
]
}
まとめ
HonoとZodを組み合わせると、バリデーションのルールやエラーメッセージ、型定義がひとつのスキーマにまとまるので、入り口で不正なデータをブロックして、その後は安心して型安全なデータを使えます。型定義やチェックの手間を減らして、さくっとAPIを作りたいときに便利です!