8
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Hono × Zodの型安全なバリデーション

8
Last updated at Posted at 2026-07-22

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を作りたいときに便利です!

8
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?