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 | ネイティブ対応 | 対応なし | 対応なし |
Cloudflare WorkersなどEdge Runtimeで動かすAPI、バンドルサイズが重要なサーバーレス環境、フロントエンドと型を共有したい(tRPC的に使いたい)場合に特に有効です。Expressの豊富なミドルウェアエコシステムが必要な場合はExpressも引き続き選択肢になります。
インストールとセットアップ
# 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 を選ぶ
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(ミドルウェアで設定する値)の型を安全に扱えます。
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スキーマでリクエストを検証できます。バリデーションが通った場合のみハンドラーが実行され、バリデーション済みの値が型安全に取得できます。
npm install @hono/zod-validator zod
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()でサブルーターを組み込めます。
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 }));
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のレスポンス型の二重管理が不要になります。
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;
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など)への型安全なアクセスも簡単に設定できます。
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;
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のテストを書けます。
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 完全ガイドもご参照ください。

