この記事は ひとりCloudflareを使い倒す Advent Calendar 2025 の 3 日目です
「Cloudflare Workers を使うなら、素で TypeScript を書いてもいいですけど、やっぱり Hono じゃないですか?!」
私は普段 Hono のお世話になっています。
ただ、なんとなくで使っているので、結局何を使っているのかわかっていません。
なので、Hono についてと Cloudflare Workers の関わりについて、自分なりに理解したいと思います。
この記事では、Hono とは何なのか (Router と Middleware を見る)を調べてまとめてみます。
この記事は、単純に筆者が調べてアウトプットすることで、記憶の定着を図るためのものです。
Hono とは
Hono は yusukebe さんが Creator の Web フレームワークです。
特に、ルーティングについては異常なまでに速い(らしい)です。
ということで、Hono のドキュメント(とソースコードも少し)を読んでいきます。
先ほども少し触れましたが、Hono は Web フレームワークです。
Web フレームワークってのは、読んで字のごとく Web のフレームワークです。
Web と一概に言ってもクライアントサイドだのサーバーサイドだの色々あるわけですが、MDN の説明で見れば Hono は由緒正しい Web フレームワークですね。
サーバーサイドウェブフレームワーク (別名「ウェブアプリケーションフレームワーク」)は、ウェブアプリケーションの作成、保守、および拡張を容易にするソフトウェアフレームワークです。適切なハンドラーへの URL のルーティング、データベースとのやり取り、セッションとユーザー認証のサポート、出力のフォーマット (HTML、JSON、XML など)、ウェブ攻撃に対するセキュリティの向上など、一般的なウェブ開発タスクを簡素化するツールとライブラリーを提供します。
後でちゃんとまとめますが、Hono はルーティングやデータベースとのやり取りなど、MDN で言っていることがほとんどできるわけです。
ルーティング
Hono のメイン機能はなんと言ってもルーティングでしょう。1
Hono のルーティングの仕組み
ルーティングの実装を読む前に、Hono がどうやってルーティングしているのかが気になりました。
ので、見ていきます。
ここからは GPT-5 をお供につけて、コード理解のお手伝いをしてもらっています。
ここでは、deno テンプレートを使って実装を追って行きます。
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => {
return c.text('Hello Hono!')
})
Deno.serve(app.fetch)
そもそも Deno について知らないので調べた
https://docs.deno.com/runtime/
Deno は JS / TS / WASM ランタイムの一つです。
V8 と Rust (+ Tokio) で実装されています。
本望ではないですが、全部端折ってDeno.serve() について調べます。
Deno.serve() は ハンドラ (ここでは app.fetch) を受け取って、サーバーをホストしてくれます。
Deno.serve を実装していそうなコードについて拾っていきます。
さっきのコードに、次の一文を追加します。
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => {
return c.text('Hello Hono!')
})
Deno.serve(app.fetch)
+ console.debug(Deno.serve.toString())
ターミナルを見ていると、serve 関数についての実装が返ってきます。
そして Deno のコードを探してみますと、ありました。
Deno.serve() を実行すると serveInner() が返ってくるということがわかりました。
serveInner() の実装が Deno.serve() のすぐ下にあります。
serveInner() はオプションを整理してserveHttpOnListener() を返します。
serveHttpOnListener() は handler を混ぜた callback を mapToCallback() で作って、serveHttpOn() を返します。
で、この serveHttpOn() で on_http_try_wait() を呼んで、リクエストを捌くようにしているんだと思います(これについては理解が及びませんでした… ChatGPT 解説です…。)
このコードを実行して、http://localhost:8000/ に GET リクエストを送ると Hello Hono! が返ってきます。
シンプルですね。
Deno.serve() に投げている app.fetch は関数です。
中身を見てみます。
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => {
return c.text('Hello Hono!')
})
Deno.serve(app.fetch)
+ console.debug(app.fetch.toString())
Listening on http://localhost:8000/
(request, ...rest)=>{
return this.#dispatch(request, rest[1], rest[0], request.method);
}
this.#dispatch() を返すことがわかります。追ってみましょう。
まず path を取ってきていますね。
で、ここからがルーターの話です。
ルーターにメソッドとパスを渡して、マッチさせています。(これはどのフレームワークでも考え方は同じ)
実はこのルーター、Hono のインスタンスを作ったときにオプションを渡していなければ勝手に設定してくれています。
SmartRouter、中身に RegExpRouter と TrieRouter が設定されています。
Hono のルーター
ルーターにルートを追加する方法について
そもそもルーターがルートを知っていないとどうにもなりません。
サンプルコードでは app.get('/' ,(c) => {...}) で追加していますが、そもそもどうやって登録されているんでしょうか。
答えはコンストラクタに書いてありました。
L125 の this[method] = (...) => {...} で、各メソッドにルート設定用の関数を入れていますね。
で、this.#addRoute を呼んでいますね。
そして path の整形とかをして、L381 で this.router.add() 呼んでルーターにルートを設定してるんですね。
ルーターに設定するところまでは理解しました。
ということで、Hono が提供している 5 つのルーターを見ていきます。
RegExp (正規表現) ルーター
ルーティングに正規表現を使ってマッチングするルーターです。
RegExpRouter is the fastest router in the JavaScript world. ――― Hono > Routers > RegExpRouter
とのことで、詳しくドキュメントを読みます。
Although this is called "RegExp" it is not an Express-like implementation using path-to-regexp. They are using linear loops. Therefore, regular expression matching will be performed for all routes and the performance will be degraded as you have more routes.
これは「RegExp」と呼ばれていますが、path-to-regexpを使用したExpressのような実装ではありません。Expressは線形ループを使用しています。そのため、すべてのルートに対して正規表現マッチングが実行され、ルートが増えるほどパフォーマンスが低下します。(DeepL 訳 筆者編)
Hono は各ルートをテストする代わりに、大きい正規表現を 1 つ作ってテストします。
実装を見ていきましょう。
まず RegExpRouter クラスがあります。
ルートの追加には this.router.add() を使うというのは先程わかりましたね。
なので、add() を見つけま、した↓
ここで勘違いしてはいけないのは、add() はあくまでもルートを追加するだけということです。
実際に大きい正規表現を作成するのは、match が呼ばれたときです。
まず、matcher.ts の match が呼ばれます。
で、buildAllMatchers() が呼ばれます。(this コンテキストで呼べます)
で、呼ばれたのがこちらです。
this.#buildMatcher(method) が L214 で呼ばれていて、呼ばれたのがこちらです。
特に、メソッドごとに呼ばれる buildMathcerFromPreprocessedRoutes が大事ですね。
独自実装されている Trie 木 と Node (Not node.js) が仕事をするわけです。
まず、path がパスパラメータを使っていないか、またはワイルドカードを使っていないか、で静的ルートを振り分けます。
そして、動的だったら Trie 木に登録してもらいます。(静的 path でも insert するけど、登録操作まではされていない)
で、path のパラメータにインデックス振って、ハンドラにもインデックスを振ってインデックスの対応表を作ってもらいます。
そして正規表現も作ります。
で、正規表現とハンドラデータのマッピングと静的ルートマッチ用のマッピングデータを一気に Matcher として返して、終わりです。
あとは match するだけです。
やってるのは
- 静的ルート検索
- 動的ルート検索 (クソデカ Regex マッチ)
- (おまけ) this.match() を build 済みに差し替える
でハンドラを返します。
おわり!!!!(カロリーすごい使った)
Trie ルーター
独自実装の Trie 木 を使ってルーティングするルーターです。
Trie 木を頑張って生やして、一発で対応するハンドラを見つけよう!ってのは変わらないようです。
ただ、Trie 木をそのまま使うので、RegExp に起こせないルーティングも表現できるのがポイントで、かつ Express のルーターよりは依然として速いそうです。
Linear ルーター
RegExp も Trie も、頑張って木を生やしたりするところから始めないとルーターが立ちません。
つまり、エッジとかで毎度最初からルーターを設定する場合には、上の 2 つはどちらも遅くなる可能性が高いです。
そこで、Linear ルーターの出番です。登録が速いので、毎度最初からの設定が一番速いと。
なので、毎度設定からやったテストケースだと、他のルーティングライブラリよりバリ早という結果が出るんですね。
Pattern ルーター
for 文でルートを探しに行くので一番遅いですが、一番軽量なルーターです。
60 行しかねえ。
Smart ルーター
他のルーターとはベクトルが違うルーターです。
複数のルーターを使ってルーティングするスマートなルーターです (ちょっと違うけど)。
思い出してください。SmartRouter はデフォルトで使用されていて、中身に RegExpRouter と TrieRouter が設定されています。
どうやってルーターを切り替えているんでしょうか。
ちょくちょく出てきましたが、add() でのルート追加は、ただルートを追加するだけです。
ルーターを作るタイミングは、パスマッチングする match() を最初に呼び出したときです。
このとき、ルーターの作成過程でエラーが出たらルーターを切り替えます。
デフォルトでは RegExp ルーター を作って、エラーが起きたら Trie 木ルーターを作って使用し続けます。
RegExp ルーターが壊れる例は以下です。
app.get("/webhooks/github", (c) => {
return c.text("Hello Hono from GitHub");
});
app.get("/webhooks/:service", (c) => {
return c.text("Hello Hono from Service");
});
こういうとき、デフォルトの設定であれば「RegExp ルーターを作ったら壊れたなあ」って「ほな Trie ルーター使うか」って切り替えてくれます。
ミドルウェア
Hono はミドルウェアにも対応しています。
ミドルウェアは、先程定義した app.get() など、ハンドラの前後に実施されます。
あんまりわからなかったので、より詳しく見ていきます。
ハンドラは Response オブジェクトを返さないといけないもの。そして、各エンドポイントで 1 つしか呼べません。
対してミドルウェアは、各エンドポイントで複数呼べます。
複数呼ばれる例として、複数のハンドラを違う階層においている故に呼ばれるケースがあります。
具体的には次のコードです。
// https://hono.dev/docs/guides/middleware#definition-of-middleware より
// match any method, all routes
app.use(logger())
// specify path
app.use('/posts/*', cors())
// specify method and path
app.post('/posts/*', basicAuth())
これで、/posts/new とかに post すると、logger() -> cors() -> basicAuth() -> *handler* の順番で処理されます。
ミドルウェアを使うと、ロガーを実装したり、Response オブジェクトをいじったりできます。
Response をいじる例として、いの一番にヘッダーが思いつきますね。
例えば以下のコードでは、レスポンスに x-message ヘッダーが追加されます。
// https://hono.dev/docs/guides/middleware#custom-middleware より
// Add a custom header
app.use('/message/*', async (c, next) => {
await next()
c.header('x-message', 'This is middleware!')
})
c.header() でヘッダーをいじっていますが、実際にいじるオブジェクトは Request と Response がくっついている Context で、これは独自実装されています。
Hono が提供するミドルウェアは、やはり外部ライブラリに依存せずで実装されています。
なお、外部ライブラリに頼ると認証系 (Cloudflare Accessなど) やバリデーター、OpenAPIなど、めちゃめちゃ多機能になります。
火炎レベルの Hono
Web 標準だからどのランタイムでも動いて、軽量で、速いルーターを持つということがわかりました。
Middleware にも標準で対応して、サードパーティ製ミドルウェアを使えばなんでもできる気がします。
ちょっとしたバックエンドにも、ある程度大きい規模にも応用が効きそうです。
この後の本アドカレでも、実際にたくさん使ってみたいと思います。
Hona。
