Express vs Hono — どちらのWebフレームワークを選ぶべきか?
1. 結論
新規プロジェクトで軽量・高速なAPIを構築するなら Hono、既存のNode.jsエコシステムとの互換性や豊富なミドルウェア資産を活かしたいなら Express を選ぶのが現時点での最適解です。Hono は Web Standards ベースでマルチランタイム対応という次世代の設計思想を持ち、Express は10年以上の実績と圧倒的なエコシステムを誇ります。プロジェクトの要件・デプロイ先・チームの習熟度に応じて選択してください。
2. 比較表
| 観点 | Express | Hono |
|---|---|---|
| 初回リリース | 2010年 | 2022年 |
| 最新バージョン(2025年時点) | v5.x(v4.x が主流) | v4.x |
| バンドルサイズ(minified) | 約 200 KB+ | 約 14 KB(hono/tiny で約 3 KB) |
| TypeScript 対応 | @types/express が必要 | ネイティブ対応(型推論が強力) |
| ランタイム対応 | Node.js(主) | Node.js / Deno / Bun / Cloudflare Workers / AWS Lambda / Vercel 等 |
| ベース仕様 | Node.js http モジュール独自API | Web Standards(Request / Response / fetch) |
| ルーティング性能 | 十分高速(RegExp ベース) | 非常に高速(Trie ベースの RegExpRouter) |
| ミドルウェア数 | npm 上に数千規模 | 公式・コミュニティ合わせて急成長中(数十〜百規模) |
| 学習コスト | 低(情報量が圧倒的) | 低〜中(Express 経験者なら移行しやすい) |
| バリデーション統合 | 外部ライブラリ(joi, zod 等)を自前で組み込み | Zod / Valibot との公式統合ヘルパーあり |
| RPC 機能 | なし | hono/client による型安全な RPC |
| JSX サポート | なし | 組み込み(hono/jsx) |
| GitHub Stars(2025年時点) | 約 66,000+ | 約 22,000+(急成長中) |
| 週間ダウンロード数 | 約 3,500万+ | 約 100万+(急成長中) |
| ライセンス | MIT | MIT |
3. それぞれの強み
Express の強み
- 圧倒的なエコシステム: passport(認証)、multer(ファイルアップロード)、cors、helmet など、あらゆるユースケースに対応するミドルウェアが揃っています。
- 情報量の多さ: Stack Overflow の回答数、ブログ記事、書籍、Udemy 等の教材が桁違いに多く、トラブルシューティングで困ることがほぼありません。
- 安定性と実績: Fortune 500 企業を含む無数のプロダクションで稼働しており、長期運用のノウハウが蓄積されています。
- 採用市場での優位性: 「Express 経験あり」は Node.js エンジニアの事実上の共通言語であり、チームビルディングがしやすいです。
- 柔軟なアーキテクチャ: "unopinionated" の名の通り、ディレクトリ構成やパターンを自由に設計できます。
Hono の強み
- マルチランタイム対応: 一つのコードベースで Cloudflare Workers、Deno Deploy、Bun、AWS Lambda、Vercel Edge Functions など多様な環境にデプロイできます。
- Web Standards 準拠:
Request/Response/fetchAPI といった標準仕様に基づいているため、ランタイムのロックインを避けられます。 - 圧倒的な軽量さ: 最小構成で約 3 KB。エッジコンピューティングやサーバーレス環境でのコールドスタートに大きく貢献します。
- TypeScript ファーストの型推論: ルーティングパラメータ、バリデーション結果、ミドルウェアのコンテキストまで型が自動推論されます。
- 型安全な RPC クライアント:
hono/clientを使えば、サーバー側のルート定義からクライアント側の型を自動生成でき、tRPC のような開発体験が得られます。 - 組み込み JSX: React や Preact なしで JSX を使ったサーバーサイドレンダリングが可能です。
- 高速なルーティング: ベンチマークで Express を大幅に上回るパフォーマンスを示しています。
4. コード例で比較
4-1. 基本的な REST API
Express
// express-app.ts
import express, { Request, Response } from "express";
const app = express();
app.use(express.json());
interface User {
id: string;
name: string;
email: string;
}
const users: User[] = [
{ id: "1", name: "田中太郎", email: "tanaka@example.com" },
{ id: "2", name: "佐藤花子", email: "sato@example.com" },
];
// 一覧取得
app.get("/api/users", (_req: Request, res: Response) => {
res.json(users);
});
// 個別取得
app.get("/api/users/:id", (req: Request, res: Response) => {
const user = users.find((u) => u.id === req.params.id);
if (!user) {
res.status(404).json({ message: "User not found" });
return;
}
res.json(user);
});
// 新規作成
app.post("/api/users", (req: Request, res: Response) => {
const { name, email } = req.body;
// バリデーションは自前で実装する必要がある
if (!name || !email) {
res.status(400).json({ message: "name and email are required" });
return;
}
const newUser: User = {
id: String(users.length + 1),
name,
email,
};
users.push(newUser);
res.status(201).json(newUser);
});
app.listen(3000, () => {
console.log("Express server running on http://localhost:3000");
});
Hono
// hono-app.ts
import { Hono } from "hono";
import { serve } from "@hono/node-server";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";
const app = new Hono();
interface User {
id: string;
name: string;
email: string;
}
const users: User[] = [
{ id: "1", name: "田中太郎", email: "tanaka@example.com" },
{ id: "2", name: "佐藤花子", email: "sato@example.com" },
];
// 一覧取得
app.get("/api/users", (c) => {
return c.json(users);
});
// 個別取得
app.get("/api/users/:id", (c) => {
const user = users.find((u) => u.id === c.req.param("id"));
if (!user) {
return c.json({ message: "User not found" }, 404);
}
return c.json(user);
});
// 新規作成(Zod バリデーション統合)
const createUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
app.post(
"/api/users",
zValidator("json", createUserSchema),
(c) => {
const { name, email } = c.req.valid("json"); // ← 型が自動推論される
const newUser: User = {
id: String(users.length + 1),
name,
email,
};
users.push(newUser);
return c.json(newUser, 201);
}
);
serve({ fetch: app.fetch, port: 3000 }, () => {
console.log("Hono server running on http://localhost:3000");
});
ポイント: Hono では
zValidatorを使うことで、バリデーションと型推論がルート定義に自然に統合されます。Express では同等のことを実現するために、ミドルウェアを自作するか外部ライブラリを別途組み込む必要があります。
4-2. ミドルウェア(認証)の実装
Express
import express, { Request, Response, NextFunction } from "express";
// カスタム型の拡張が必要
declare global {
namespace Express {
interface Request {
userId?: string;
}
}
}
function authMiddleware(req: Request, res: Response, next: NextFunction) {
const token = req.headers.authorization?.replace("Bearer ", "");
if (!token || token !== "valid-token") {
res.status(401).json({ message: "Unauthorized" });
return;
}
req.userId = "user-123"; // 型拡張が必要
next();
}
const app = express();
app.get("/api/profile", authMiddleware, (req: Request, res: Response) => {
// req.userId の型は string | undefined(型安全ではない)
res.json({ userId: req.userId });
});
Hono
import { Hono } from "hono";
import { createMiddleware } from "hono/factory";
// ミドルウェアで設定する変数の型を定義
type AuthEnv = {
Variables: {
userId: string;
};
};
const authMiddleware = createMiddleware<AuthEnv>(async (c, next) => {
const token = c.req.header("Authorization")?.replace("Bearer ", "");
if (!token || token !== "valid-token") {
return c.json({ message: "Unauthorized" }, 401);
}
c.set("userId", "user-123"); // 型安全に変数をセット
await next();
});
const app = new Hono();
app.get("/api/profile", authMiddleware, (c) => {
const userId = c.get("userId"); // ← string 型として推論される
return c.json({ userId });
});
ポイント: Express では
Requestオブジェクトの型拡張(declare global)が必要で、型安全性に限界があります。Hono ではVariables型をジェネリクスで渡すことで、ミドルウェアが設定した値をハンドラ側で型安全に取得できます。
4-3. Hono 独自機能 — 型安全 RPC クライアント
// server.ts(Hono サーバー側)
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";
const app = new Hono()
.get("/api/users", (c) => {
return c.json([
{ id: "1", name: "田中太郎" },
{ id: "2", name: "佐藤花子" },
]);
})
.post(
"/api/users",
zValidator("json", z.object({ name: z.string(), email: z.string().email() })),
(c) => {
const body = c.req.valid("json");
return c.json({ id: "3", ...body }, 201);
}
);
// 型をエクスポート
export type AppType = typeof app;
// client.ts(クライアント側 — フロントエンドや別サービス)
import { hc } from "hono/client";
import type { AppType } from "./server";
const client = hc<AppType>("http://localhost:3000");
async function main() {
// GET /api/users — レスポンスの型が自動推論される
const res = await client.api.users.$get();
const users = await res.json();
// users の型: { id: string; name: string }[]
// POST /api/users — リクエストボディの型もチェックされる
const createRes = await client.api.users.$post({
json: { name: "鈴木一郎", email: "suzuki@example.com" },
});
const newUser = await createRes.json();
// newUser の型: { id: string; name: string; email: string }
}
ポイント: Express には同等の機能がなく、tRPC や OpenAPI + コード生成ツールなど別のソリューションを組み合わせる必要があります。
5. どちらを選ぶべきか — ユースケース別の推奨
Express を選ぶべきケース
| ユースケース | 理由 |
|---|---|
| 既存の Express アプリの保守・拡張 | 移行コストに見合わないケースが多い |
| passport 等の特定ミドルウェアに強く依存 | Hono に同等のエコシステムがまだない |
| チームに Express 経験者が多い | 学習コストゼロで即戦力になる |
| Node.js 固有の API(Stream, Buffer 等)を多用 | Express の方が自然に扱える |
| 大量の日本語・英語ドキュメントが必要 | 情報量で Express が圧倒的に優位 |
Hono を選ぶべきケース
| ユースケース | 理由 |
|---|---|
| **Cloudflare Workers / Deno |