ブログへ戻る

Hono vs Express 5:同じAPIを両方で実装してベンチマークした

TypeScript APIをExpress 5とHono 4の両方で実装し、Node.js 22でベンチマークを取った。純粋なルーティング速度はHonoが2.25倍速いが、5msの非同期処理を加えると差は5%未満になる。速度以外の選択基準を実測データとともに解説する。

新しいTypeScript APIを始めるとき、Express 5とHonoのどちらを選ぶべきか。2026年時点では、これは「古いフレームワークか新しいフレームワークか」ではなく、設計思想の選択になっている。

Express 5は2025年3月にnpmのデフォルトlatestになり、プロジェクトとしても活発に動いている。一方Honoは、Cloudflare Workersに特化したルーターから、Cloudflare Workers・Cloudflare Pages・Node.js・Deno・Bun・AWS Lambda・Vercel・Fastly Computeに公式アダプターを提供するフレームワークへと成長した。

「Honoは速い」という記事は多い。しかし、その速度差が実際のアプリケーションでどれだけ意味を持つかは、ほとんど検証されていない。両方のフレームワークで同じAPIを実装し、Node.js 22でベンチマークを取った結果を共有する。

結論から先に

| 状況 | 選択 | |---|---| | 新規TypeScript API、ランタイム移植性が必要 | Hono | | Cloudflare Workers、Deno、Bun | Hono | | 依存パッケージを最小にしたい | Hono | | 既存のExpressコードベース | Expressのまま | | Node.js専用、middlewareが多い | Express | | Express 4からのアップグレード | Express 5(Honoへの移行より低リスク) |

2026年にこの比較が変わった理由

「Expressはオワコン、Honoを使え」という記事が増えた時期があった。しかしExpress 5のリリースで状況が変わっている。

Express 5の主な変更点:

  • 非同期ミドルウェアのエラーが自動的にエラーハンドラーへ渡される(try/catchでラップしなくてよい)
  • path-to-regexp v8への更新
  • LTSプランの公開

Express 5は「Expressが復活した」というより、「長年放置されていた課題を解消したメンテナンスリリース」に近い。ただしそれによって、新規プロジェクトでExpressを選ぶ合理性が再び生まれた。

設計の違い:Node.jsネイティブ vs Web Standards

ExpressはNode.jsのhttp.IncomingMessagehttp.ServerResponseを直接扱う。Node.js専用の設計だが、変換オーバーヘッドがない。

HonoはWeb Fetch API(RequestResponse)を中心に設計されている。ブラウザ・Cloudflare Workers・Deno・Node.jsで共通の仕様だ。Node.js上でHonoを動かすには@hono/node-serverというアダプターが必要で、このアダプターがNode.jsのhttpモジュールとFetch APIの間を橋渡しする。

この設計の違いが、後述するパフォーマンス特性・メモリ使用量・TypeScript体験・ランタイム移植性のすべての根拠になる。

実装:同じAPIを両方で作った

比較のために、以下のAPIを両フレームワークで実装した。

GET  /health          — ヘルスチェック(ミドルウェアなし)
GET  /users/:id       — 認証ミドルウェア + 5msの疑似DBアクセス
POST /users           — 認証 + Zodバリデーション + 5msの疑似DBアクセス

Express 5:

import express, { Request, Response, NextFunction } from 'express'
import cors from 'cors'
import { z } from 'zod'

const app = express()
app.use(cors())
app.use(express.json())

interface AuthRequest extends Request {
  user?: { id: string }
}

function auth(req: AuthRequest, res: Response, next: NextFunction) {
  if (!req.headers['authorization']) return res.status(401).json({ error: 'Unauthorized' })
  req.user = { id: '1' }
  next()
}

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

app.get('/health', (req, res) => res.json({ status: 'ok' }))

app.get('/users/:id', auth, async (req: AuthRequest, res) => {
  await new Promise(r => setTimeout(r, 5))
  res.json({ id: req.params.id, name: 'Test User' })
})

app.post('/users', auth, async (req: AuthRequest, res) => {
  const result = CreateUserSchema.safeParse(req.body)
  if (!result.success) return res.status(400).json({ error: 'Invalid' })
  await new Promise(r => setTimeout(r, 5))
  res.status(201).json({ id: '123', ...result.data })
})

Hono 4:

import { Hono, type MiddlewareHandler } from 'hono'
import { cors } from 'hono/cors'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'

type Variables = { user: { id: string } }
const app = new Hono<{ Variables: Variables }>()

app.use('*', cors())

const auth: MiddlewareHandler<{ Variables: Variables }> = async (c, next) => {
  if (!c.req.header('authorization')) return c.json({ error: 'Unauthorized' }, 401)
  c.set('user', { id: '1' })
  await next()
}

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

app.get('/health', (c) => c.json({ status: 'ok' }))

app.get('/users/:id', auth, async (c) => {
  await new Promise(r => setTimeout(r, 5))
  return c.json({ id: c.req.param('id'), name: 'Test User' })
})

app.post('/users', auth, zValidator('json', CreateUserSchema), async (c) => {
  const data = c.req.valid('json')
  await new Promise(r => setTimeout(r, 5))
  return c.json({ id: '123', ...data }, 201)
})

コード量は同程度。違いはTypeScript体験で現れる。

TypeScript体験の差

Expressでは、ミドルウェアがreqオブジェクトに追加したプロパティ(例:req.user)に型をつけるために、インターフェースの拡張が必要になる。

// 型は手動で定義する
interface AuthRequest extends Request {
  user?: { id: string }
}

// ルートハンドラーでキャスト
app.get('/users/:id', auth, async (req: AuthRequest, res) => {
  // authミドルウェアが実行済みかどうか、TypeScriptは確認できない
  const user = req.user
})

ミドルウェアチェーンに型情報は伝わらない。authが実際に実行されたかどうかはどちらのフレームワークもコンパイル時には検証できない。Honoの優位は検証ではなく、型を得るためのボイラープレートが不要という点にある。

Honoではアプリのジェネリクスでコンテキストの型を定義する。インターフェース拡張は不要だ。

type Variables = { user: { id: string } }
const app = new Hono<{ Variables: Variables }>()

// インターフェース拡張なしで c.get('user') が { id: string } を返す
app.get('/users/:id', auth, async (c) => {
  const user = c.get('user') // { id: string }
})

バリデーションも同様で、zValidatorを通すとc.req.valid('json')がZodスキーマから推論された型を返す。Expressのreq.bodyは検証後もanyのまま。

TypeScriptを重視するチームにとって、Honoのジェネリクス設計はインターフェース拡張やキャストの記述量を減らす。

ミドルウェアエコシステム

Expressの最も強い優位点はここにある。

helmetexpress-rate-limitpassportmulterexpress-sessionmorganなど、長年本番環境で使われてきたミドルウェアが豊富にある。サードパーティのデータベースクライアントや認証プロバイダーも、Expressミドルウェアとして統合を提供していることが多い。

Honoにも標準ミドルウェアは揃っている(CORS・ロギング・JWT・レートリミット・キャッシュ制御など)。一般的なユースケースは標準ミドルウェアで対応できる。ただし、Passportのストラテジーや複雑なセッション管理など、Expressの固有ミドルウェアに相当するものはまだ少ない。

Honoの標準ミドルウェアで要件が満たせる場合、エコシステムの差は小さい。特定のExpressミドルウェアが必要な場合は、移行コストに含めて考える必要がある。

ベンチマーク結果

環境:

| 項目 | 値 | |---|---| | Node.js | v22.17.1 | | Express | 5.2.1 | | Hono | 4.13.1 | | @hono/node-server | 1.19.17 | | ベンチマークツール | autocannon、同時接続10 | | 計測時間 | 各シナリオ10秒 | | プラットフォーム | macOS、Apple Silicon(arm64) | | 日付 | 2026-08-08 |

ロギングミドルウェアは計測中無効にした。両サーバーは別プロセスで起動した。各シナリオの計測は1回(10秒連続計測)。Linux x86環境では絶対値が異なる可能性があるが、相対的な傾向は変わらないと見ている。

結果:

| シナリオ | フレームワーク | req/s | p50 (ms) | p99 (ms) | |---|---|---|---|---| | A: GET /health(純粋ルーティング) | Express 5 | 4,705 | 2 | 6 | | A: GET /health(純粋ルーティング) | Hono 4 | 10,579 | 0 | 2 | | B: GET /users/:id(認証 + 5ms非同期) | Express 5 | 1,502 | 6 | 10 | | B: GET /users/:id(認証 + 5ms非同期) | Hono 4 | 1,575 | 6 | 12 | | C: POST /users(認証 + Zod + 5ms非同期) | Express 5 | 1,511 | 6 | 17 | | C: POST /users(認証 + Zod + 5ms非同期) | Hono 4 | 1,550 | 6 | 14 |

数字が示すこと:

シナリオAは、フレームワーク固有のルーティングオーバーヘッドのみを計測している。Honoは2.25倍速い。

しかしシナリオBとCが本質を示す。5msの非同期処理(典型的なDBクエリ1回に相当)を追加した途端、差は4.9%と2.6%になる。1,500 req/s換算で約70〜75 req/sの差だ。

ほとんどのAPIエンドポイントは実際の処理を行う。DBクエリ・外部API呼び出し・シリアライズ・ビジネスロジックがレイテンシの大部分を占める。フレームワークのルーティングオーバーヘッドは、5ms程度の非同期処理が入ると誤差の範囲に収まる。

メモリ使用量(RSS、ベンチマーク後):

| フレームワーク | RSS | |---|---| | Express 5 | 51 MB | | Hono 4(Node.js) | 133 MB |

これは直感に反する結果だ。HonoはCloudflare Workersでは非常に軽量だが、Node.js上では@hono/node-serverアダプターがFetch APIとNode.jsのhttpモジュールを橋渡しするため、その分のオーバーヘッドが加わる。ExpressはNode.jsネイティブAPIを直接使用するため、RSS消費が少ない。

注意:RSSはベンチマーク完了後に1回計測したもの。GCの状態によって変動する。Cloudflare Workers上では、Honoはアダプターなしで動作し、フットプリントは大幅に小さくなる。

依存パッケージ数

| フレームワーク(cors + zod含む) | 合計パッケージ数 | node_modulesサイズ | |---|---|---| | Express 5 | 68 | 8.8 MB | | Hono 4 | 4 | 8.1 MB |

Expressは自身の直接依存として28パッケージを持つ(body-parseracceptstype-isなど)。これらの推移的依存を合わせると68パッケージになる。

Honoのコアパッケージは外部依存がゼロ。@hono/node-server@hono/zod-validatorzodを加えても4パッケージ。

依存の少なさは、サーバーレスやエッジ環境でのコールドスタートに直接影響する。Node.jsの常時稼働サーバーでは起動時間の差は小さいが、AWS LambdaやCloudflare Workersでは意味を持つ。

ランタイム移植性

依存パッケージの差はHonoの最大の設計上の優位点につながる。同じアプリケーションコードが、複数のランタイムで動作する。

ExpressはNode.js APIに依存して書かれる。Cloudflare WorkersやDeno、Bunへ移行する場合、サーバーエントリーポイントだけでなくフレームワーク層全体の書き直しが必要になる。

Honoはルートハンドラー内でWeb Standard APIを使う。c.reqはCloudflare WorkersのRequestオブジェクトと同じ仕様だ。デプロイ先を変えるときは、エントリーポイントのアダプターを差し替えるだけでよい。

Node.js:

import { serve } from '@hono/node-server'
import { app } from './app'
serve({ fetch: app.fetch, port: 3000 })

Cloudflare Workers:

import { app } from './app'
export default { fetch: app.fetch }

app.tsのルートハンドラーは変更しない。Node.jsからエッジへの移行を将来の選択肢として残したいチームには、これは理論上の優位ではなく実際の利点になる。

詳細は「Cloudflare PagesからWorkersへの移行」を参照。HonoをWorkers上でDeployする手順も含んでいる。

既存のExpressアプリは移行すべきか

パフォーマンスだけを理由にした移行は勧めない。

実際の処理が入るシナリオでは、両フレームワークの差は5%以内だった。Honoへ移行しても遅いエンドポイントは速くならない。ボトルネックがDBクエリやキャッシュミスにある場合、フレームワーク層は制約ではない。

移行を検討する正当な理由:

  • Cloudflare WorkersやDeno等のランタイム移植性が必要
  • ミドルウェアチェーンを通じたTypeScript型安全性が重要
  • 依存パッケージを最小化したい(68→4)

Expressに留まる正当な理由:

  • 利用中のExpressミドルウェアに相当するHono実装がない
  • Express 4→5のアップグレードで現在の課題が解決する(非同期エラーハンドリングなど)
  • フレームワーク移行のコストがHonoの利点を上回る

Express 5には公式マイグレーションガイドがある。Express内でのアップグレードを先に検討する価値がある。

Fastifyについて

Node.js専用APIでは、Fastifyも有力な選択肢だ。JSONスキーマを活用したバリデーションとシリアライズが特徴で、プラグインエコシステムも成熟している。Node.js上の純粋なルーティング速度はExpressより大幅に速いとするベンチマーク結果が多い。なお本記事ではFastifyの計測は行っていない。

Fastifyが向くケース:Node.js専用で最大スループットが必要、JSONスキーマが許容できる。

HonoとFastifyの主な違い:FastifyはExpressと同様にNode.jsネイティブ設計。Cloudflare WorkersやDenoへの移行は、Honoと比べて大きな書き直しを伴う。ランタイム移植性の観点では、FastifyはExpressと同じカテゴリーに入る。

選択基準のまとめ

Honoを選ぶ場合:

  • Cloudflare Workers・Deno・Bun、またはその将来の可能性が必要
  • TypeScriptのミドルウェアチェーン型推論が重要
  • 依存パッケージを最小化したい
  • 新規APIで既存の資産がない

Expressを選ぶ場合:

  • 特定のExpressミドルウェアが必要でHono版がない
  • 既存のExpressコードベースをアップグレードする
  • チームのExpressノウハウが深く、移行コストが利点を上回る
  • Node.jsのみで運用、エッジランタイムの予定がない

移行しない場合:

  • 移行理由がパフォーマンスだけ — 実際の差は5%未満
  • ランタイム移植性もTypeScript DXの改善も必要がない
  • 現在使用中のExpressミドルウェアのHono版がなく、代替実装コストが大きい

Honoを使ってSupabase AuthのJWT検証を実装する手順は「Hono Supabase Auth: JWTを検証する4つの方法」に詳しい。

ベンチマーク環境:Node.js v22.17.1、Express 5.2.1、Hono 4.13.1、macOS arm64、2026-08-08。