【TypeScript × Hono】完全ガイド|型安全なルーティング・ミドルウェア・Zod連携・RPC Client・Cloudflare Workers対応まで徹底解説

【TypeScript × Hono】完全ガイド|型安全なルーティング・ミドルウェア・Zod連携・RPC Client・Cloudflare Workers対応まで徹底解説 TypeScript

TypeScriptでAPIやWebアプリを開発するとき、Expressは長らく定番でした。しかし近年、Honoという超軽量なWebフレームワークが急速に注目を集めています。HonoはTypeScriptを前提に設計されており、型安全なルーティング・ミドルウェア・RPCクライアントを標準で備えています。

さらにHonoはCloudflare Workers・Deno・Bun・Node.js・AWS Lambdaなど複数のランタイムで動作する点も大きな特徴です。本記事ではHonoの基礎から実務で使える応用パターンまでを、TypeScriptの型安全性に焦点を当てて解説します。

この記事でわかること

  • Honoの設計思想とExpressとの違い
  • 型安全なルーティングの定義方法
  • ミドルウェアとContextへの型の付け方
  • Zodを使ったリクエストバリデーション(zValidator)
  • hono/clientを使ったRPCスタイルの型安全なAPIクライアント
  • Cloudflare WorkersとNode.jsへのデプロイ
  • エラーハンドリングとテストの書き方

Zodの基礎についてはTypeScript × Zod 完全ガイドを、ExpressによるREST API構築についてはTypeScript × Express 完全ガイドもあわせてご覧ください。

スポンサーリンク

Honoとは・なぜ選ばれるのか

HonoはYusuke Wada氏が開発した、Web標準API(Fetch API)ベースの超軽量Webフレームワークです。バンドルサイズは14KB以下で、ゼロ依存のシンプルな設計を持ちます。

比較項目 Hono Express Fastify
TypeScript対応 ネイティブ(型安全設計) @types/expressが必要 ジェネリクスで対応
バンドルサイズ 〜14KB 〜200KB+ 〜100KB+
ランタイム CF Workers/Node/Deno/Bun等 Node.jsのみ Node.jsのみ
Web標準準拠 Request/Response準拠 独自オブジェクト 独自オブジェクト
RPCクライアント 標準搭載(hono/client) なし なし
ミドルウェア 豊富な公式ミドルウェア 豊富(サードパーティ) プラグイン方式
Edge Runtime ネイティブ対応 対応なし 対応なし
Honoが特に向いているケース
Cloudflare WorkersなどEdge Runtimeで動かすAPI、バンドルサイズが重要なサーバーレス環境、フロントエンドと型を共有したい(tRPC的に使いたい)場合に特に有効です。Expressの豊富なミドルウェアエコシステムが必要な場合はExpressも引き続き選択肢になります。

インストールとセットアップ

インストール(Node.js + TypeScript)
# Node.js環境
npm create hono@latest my-app
# テンプレート選択: nodejs を選ぶ

# または手動でインストール
npm install hono
npm install --save-dev @types/node tsx

# Cloudflare Workers環境
# npm create hono@latest my-app  → cloudflare-workers を選ぶ
src/index.ts(Node.js)
import { Hono } from "hono";
import { serve } from "@hono/node-server";

const app = new Hono();

app.get("/", (c) => {
  return c.json({ message: "Hello, Hono!" });
});

serve({
  fetch: app.fetch,
  port: 3000,
}, (info) => {
  console.log(`Server running at http://localhost:${info.port}`);
});

型安全なルーティング

Honoのルーティングはメソッドチェーン形式で定義でき、パスパラメータ・クエリパラメータの型が自動推論されます。

ルーティング定義
import { Hono } from "hono";

const app = new Hono();

// --- 基本的なルート ---
app.get("/users",       (c) => c.json({ users: [] }));
app.post("/users",      (c) => c.json({ created: true }, 201));
app.put("/users/:id",   (c) => c.json({ updated: true }));
app.delete("/users/:id",(c) => c.json({ deleted: true }));

// --- パスパラメータ ---
app.get("/users/:id", (c) => {
  const id = c.req.param("id"); // 型: string
  return c.json({ id });
});

// --- 複数パラメータ ---
app.get("/users/:userId/posts/:postId", (c) => {
  const { userId, postId } = c.req.param();
  // 型: { userId: string; postId: string }
  return c.json({ userId, postId });
});

// --- クエリパラメータ ---
app.get("/search", (c) => {
  const query  = c.req.query("q")      ?? "";
  const limit  = c.req.query("limit")  ?? "20";
  const offset = c.req.query("offset") ?? "0";
  return c.json({ query, limit: Number(limit), offset: Number(offset) });
});

// --- ワイルドカード ---
app.get("/static/*", (c) => {
  return c.text("Static file: " + c.req.path);
});

Contextオブジェクトと型定義

Honoのハンドラーに渡されるc(Context)は、リクエスト・レスポンス・環境変数などへのアクセスを提供します。ジェネリクスで型を指定することで、Variables(ミドルウェアで設定する値)の型を安全に扱えます。

Contextの型定義
import { Hono, Context } from "hono";

// Variables(ミドルウェアでセットする値)の型定義
type Variables = {
  user: {
    id: string;
    email: string;
    role: "admin" | "user";
  };
};

// アプリにVariablesの型を渡す
const app = new Hono<{ Variables: Variables }>();

// ミドルウェアでuserをセット
app.use("*", async (c, next) => {
  // 認証処理(ここでは仮のユーザーをセット)
  c.set("user", { id: "u1", email: "test@example.com", role: "user" });
  await next();
});

// ハンドラーでuserを取得(型安全)
app.get("/me", (c) => {
  const user = c.get("user");
  // 型: { id: string; email: string; role: "admin" | "user" }
  return c.json(user);
});

// レスポンスヘルパー
app.get("/response-types", (c) => {
  // JSON
  // return c.json({ hello: "world" });

  // テキスト
  // return c.text("Hello!");

  // HTML
  // return c.html("<h1>Hello</h1>");

  // リダイレクト
  // return c.redirect("/new-path", 301);

  // ステータスコードを指定
  return c.json({ error: "Not Found" }, 404);
});

ミドルウェアの実装

Honoのミドルウェアはasync (c, next) => {}の形式で定義します。next()を呼ぶことで次のミドルウェアまたはハンドラーに処理が移ります。

ミドルウェア実装例
import { Hono } from "hono";
import { cors } from "hono/cors";
import { logger } from "hono/logger";
import { jwt } from "hono/jwt";
import { rateLimiter } from "hono-rate-limiter";

const app = new Hono<{ Variables: Variables }>();

// --- 公式ミドルウェア ---
app.use("*", logger());     // リクエストログ
app.use("*", cors({         // CORS設定
  origin: ["https://example.com"],
  credentials: true,
}));

// --- JWT認証ミドルウェア ---
app.use("/api/*", jwt({ secret: process.env.JWT_SECRET! }));

// --- カスタム認証ミドルウェア ---
const authMiddleware = async (c: Context<{ Variables: Variables }>, next: Next) => {
  const authHeader = c.req.header("Authorization");

  if (!authHeader?.startsWith("Bearer ")) {
    return c.json({ error: "Unauthorized" }, 401);
  }

  const token = authHeader.slice(7);

  // トークン検証(実際はJWT検証など)
  const user = await verifyToken(token);
  if (!user) return c.json({ error: "Invalid token" }, 401);

  c.set("user", user);
  await next();
};

// 特定ルートにミドルウェアを適用
app.get("/api/me", authMiddleware, (c) => {
  return c.json(c.get("user"));
});

Zodを使ったリクエストバリデーション(zValidator)

HonoはzValidatorを使ってZodスキーマでリクエストを検証できます。バリデーションが通った場合のみハンドラーが実行され、バリデーション済みの値が型安全に取得できます。

インストール(zValidator)
npm install @hono/zod-validator zod
zValidatorを使ったバリデーション
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";

const app = new Hono();

// --- リクエストボディのバリデーション ---
const createUserSchema = z.object({
  name:  z.string().min(1).max(100),
  email: z.string().email(),
  age:   z.number().int().min(0).max(150).optional(),
});

app.post(
  "/users",
  zValidator("json", createUserSchema),
  (c) => {
    const body = c.req.valid("json");
    // 型: { name: string; email: string; age?: number | undefined }
    // バリデーション済みなので安全に使える
    return c.json({ created: body }, 201);
  }
);

// --- クエリパラメータのバリデーション ---
const searchSchema = z.object({
  q:      z.string().min(1),
  limit:  z.coerce.number().int().min(1).max(100).default(20),
  offset: z.coerce.number().int().min(0).default(0),
});

app.get(
  "/search",
  zValidator("query", searchSchema),
  (c) => {
    const { q, limit, offset } = c.req.valid("query");
    // 型: { q: string; limit: number; offset: number }
    return c.json({ q, limit, offset });
  }
);

// --- パスパラメータのバリデーション ---
const paramSchema = z.object({
  id: z.coerce.number().int().positive(),
});

app.get(
  "/users/:id",
  zValidator("param", paramSchema),
  (c) => {
    const { id } = c.req.valid("param");
    // 型: { id: number }  ← 文字列ではなく数値に変換済み
    return c.json({ userId: id });
  }
);

ルーターの分割とモジュール化

アプリが大きくなるにつれ、ルートを機能ごとに分割するのが一般的です。Honoはapp.route()でサブルーターを組み込めます。

src/routes/users.ts
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";

// サブルーターを作成
export const usersRouter = new Hono();

const createSchema = z.object({
  name:  z.string().min(1),
  email: z.string().email(),
});

usersRouter
  .get("/",    (c) => c.json({ users: [] }))
  .get("/:id", (c) => c.json({ id: c.req.param("id") }))
  .post("/",   zValidator("json", createSchema), (c) => {
    const body = c.req.valid("json");
    return c.json({ created: body }, 201);
  })
  .put("/:id", (c) => c.json({ updated: true }))
  .delete("/:id", (c) => c.json({ deleted: true }));
src/index.ts(ルーターの組み込み)
import { Hono } from "hono";
import { usersRouter }  from "./routes/users";
import { postsRouter }  from "./routes/posts";

const app = new Hono();

// /api/v1 プレフィックスでサブルーターを登録
app.route("/api/v1/users", usersRouter);
app.route("/api/v1/posts", postsRouter);

export default app;

hono/client:型安全なRPCクライアント

Honoの最も強力な機能の1つがhono/clientです。サーバー側のルート定義から型を自動生成し、tRPCのように型安全なAPIクライアントを作れます。APIのレスポンス型の二重管理が不要になります。

サーバー側(型をexport)
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";

const userSchema = z.object({
  name:  z.string(),
  email: z.string().email(),
});

const app = new Hono()
  .get("/users", (c) =>
    c.json([
      { id: 1, name: "山田太郎", email: "yamada@example.com" },
    ])
  )
  .get("/users/:id", (c) =>
    c.json({ id: c.req.param("id"), name: "山田太郎" })
  )
  .post(
    "/users",
    zValidator("json", userSchema),
    (c) => c.json({ created: true }, 201)
  );

// ← ここが重要: AppRouteの型をexportする
export type AppType = typeof app;
export default app;
クライアント側(型安全なAPIコール)
import { hc } from "hono/client";
import type { AppType } from "../server";

// 型安全なクライアントを生成
const client = hc<AppType>("http://localhost:3000");

// GET /users - レスポンス型が自動推論される
const usersRes = await client.users.$get();
const users = await usersRes.json();
// 型: { id: number; name: string; email: string }[]

// GET /users/:id
const userRes = await client.users[":id"].$get({
  param: { id: "1" },
});
const user = await userRes.json();
// 型: { id: string; name: string }

// POST /users(型チェック付き)
const createRes = await client.users.$post({
  json: { name: "鈴木花子", email: "suzuki@example.com" },
  // json: { name: "鈴木花子" }  ← emailがないとコンパイルエラー
});
const result = await createRes.json();
// 型: { created: boolean }

エラーハンドリング

エラーハンドリング
import { Hono, HTTPException } from "hono";

const app = new Hono();

// --- HTTPExceptionを使ったエラー送出 ---
app.get("/users/:id", (c) => {
  const id = Number(c.req.param("id"));

  if (isNaN(id) || id <= 0) {
    throw new HTTPException(400, { message: "Invalid user ID" });
  }

  // ユーザーが見つからない場合
  const user = findUser(id);
  if (!user) {
    throw new HTTPException(404, { message: "User not found" });
  }

  return c.json(user);
});

// --- グローバルエラーハンドラー ---
app.onError((err, c) => {
  if (err instanceof HTTPException) {
    return c.json(
      { error: err.message, status: err.status },
      err.status
    );
  }

  // 想定外のエラー
  console.error(err);
  return c.json({ error: "Internal Server Error" }, 500);
});

// --- 404ハンドラー ---
app.notFound((c) => {
  return c.json({ error: `Route ${c.req.path} not found` }, 404);
});

// --- zValidatorのエラーカスタマイズ ---
app.post(
  "/users",
  zValidator("json", createUserSchema, (result, c) => {
    if (!result.success) {
      return c.json(
        { error: "Validation failed", details: result.error.flatten() },
        422
      );
    }
  }),
  (c) => { /* ... */ }
);

Cloudflare Workersへのデプロイ

HonoはCloudflare Workersと相性が抜群です。Bindings(KV・D1・R2など)への型安全なアクセスも簡単に設定できます。

Cloudflare Workers設定(src/index.ts)
import { Hono } from "hono";

// Bindingsの型定義
type Bindings = {
  DB:     D1Database;       // D1データベース
  KV:     KVNamespace;      // KVストレージ
  BUCKET: R2Bucket;         // R2オブジェクトストレージ
  SECRET: string;           // シークレット変数
};

const app = new Hono<{ Bindings: Bindings }>();

app.get("/kv/:key", async (c) => {
  const key   = c.req.param("key");
  const value = await c.env.KV.get(key);
  // c.env.KV の型: KVNamespace  ← 型安全!

  if (!value) return c.json({ error: "Key not found" }, 404);
  return c.json({ key, value });
});

app.get("/db/users", async (c) => {
  const result = await c.env.DB.prepare(
    "SELECT * FROM users LIMIT 10"
  ).all();
  return c.json(result.results);
});

// Cloudflare Workers用エクスポート
export default app;
wrangler.toml(Cloudflare Workers設定ファイル)
name = "my-hono-api"
main = "src/index.ts"
compatibility_date = "2024-01-01"

[[kv_namespaces]]
binding = "KV"
id = "your-kv-namespace-id"

[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "your-d1-database-id"

# デプロイコマンド
# npx wrangler deploy

テスト

HonoにはHTTPリクエストを模倣するtestClientが組み込まれており、外部サーバーを起動せずにAPIのテストを書けます。

テスト例(Vitest)
import { describe, it, expect } from "vitest";
import { testClient } from "hono/testing";
import app from "../src";

const client = testClient(app);

describe("GET /users", () => {
  it("ユーザー一覧を返す", async () => {
    const res = await client.users.$get();

    expect(res.status).toBe(200);
    const data = await res.json();
    expect(Array.isArray(data)).toBe(true);
  });
});

describe("POST /users", () => {
  it("有効なデータでユーザーを作成できる", async () => {
    const res = await client.users.$post({
      json: { name: "テストユーザー", email: "test@example.com" },
    });

    expect(res.status).toBe(201);
    const data = await res.json();
    expect(data.created).toBe(true);
  });

  it("不正なメールアドレスは422を返す", async () => {
    const res = await client.users.$post({
      json: { name: "テストユーザー", email: "invalid-email" } as never,
    });

    expect(res.status).toBe(422);
  });
});

describe("GET /users/:id", () => {
  it("存在しないユーザーは404を返す", async () => {
    const res = await client.users[":id"].$get({ param: { id: "9999" } });
    expect(res.status).toBe(404);
  });
});

まとめ

HonoはTypeScript開発者にとって非常に使いやすいWebフレームワークです。型安全なルーティング・ミドルウェア・RPCクライアント・バリデーションが揃っており、Cloudflare WorkersなどのEdge Runtimeにも簡単にデプロイできます。

機能 ポイント
ルーティング メソッドチェーン形式・パスパラメータ型推論
Context型定義 ジェネリクスでVariables/Bindingsの型を指定
zValidator Zodスキーマでリクエストを型安全に検証
ルーター分割 app.route()でサブルーターを組み込み
hono/client サーバー型からRPCクライアントを自動生成
エラー処理 HTTPException・onError・notFoundで一元管理
CF Workers Bindings型でKV/D1/R2を型安全に利用
テスト testClientでサーバー起動不要のユニットテスト

TypeScriptの型エラーハンドリングについてはTypeScript エラーハンドリング完全ガイド、Next.jsとの組み合わせについてはTypeScript × Next.js App Router 完全ガイドもご参照ください。