design-philosophy
リポジトリ: honojs/hono 分析日: 2026-02-14
概要
Web Standards 準拠の設計哲学と技術選定の判断基準を分析する。Hono は「Web Standards を唯一の抽象化レイヤーとして採用し、ランタイム固有 API を Adapter パターンで隔離する」という一貫した設計判断を貫いている。この判断により、ゼロ依存・マルチランタイム・超軽量という3つの目標を同時に達成している。フレームワーク設計における「何を標準とし、何をオプションにするか」の判断基準として、あらゆる規模のライブラリ設計に応用可能な知見を含む。
背景にある原則
最小公倍数ではなく最大公約数を基盤にすべき: マルチランタイム対応において、各ランタイムの機能を全て包含する抽象レイヤー(最小公倍数)ではなく、全ランタイムが共通で持つ Web Standards API(最大公約数)をコアに据えている。
fetch()のシグネチャ(request: Request, env?, executionCtx?) => Response | Promise<Response>がその象徴で、Cloudflare Workers のexport default { fetch }と同じ形式。コアはRequestin /Responseout に徹し、ランタイム固有の情報はenv経由で受け取る(src/hono-base.ts:473-479)。依存を追加する前に標準 API で実装できないか確認すべき:
package.jsonのdependenciesは空。暗号処理はcrypto.subtle(src/utils/crypto.ts:45-46)、ストリーミングはReadableStream/WritableStream/TransformStream(src/utils/stream.ts)、エンコーディングはTextEncoder(src/utils/crypto.ts:42)で実装している。Node.js 固有のcryptoモジュールやBufferを一切使わないことで、全ランタイムで動作する。パフォーマンスのために標準 API の使用を避けるべき場面を見極めよ: Web Standards に準拠しつつも、ホットパスでは標準 API のオーバーヘッドを回避している。
getPath()はnew URL()を使わず文字列の charCode 比較でパスを抽出し(src/utils/url.ts:106-134)、getQueryParam()もURLSearchParamsを使わず手動パースしている(src/utils/url.ts:219-300)。標準 API は「正しいが遅い」場面が存在する。選択肢はユーザーに委ね、デフォルトは最適解を提供すべき: ルーター実装を5種類用意し(RegExpRouter, TrieRouter, LinearRouter, PatternRouter, SmartRouter)、プリセットで使い分ける。デフォルトの
HonoクラスはSmartRouterで最初のリクエスト時に最適なルーターを自動選択する(src/router/smart-router/router.ts:21-60)。hono/tinyは最小サイズの PatternRouter を使い、hono/quickは起動が速い LinearRouter を使う。
実例と分析
Web Standards を境界契約として使う
Hono のコア API は fetch() シグネチャそのものである。エントリポイントは app.fetch(request, env, executionCtx) で、入力は Request、出力は Response。この設計により、どのランタイムでも export default app だけで動作する。
// src/hono-base.ts:473-479
fetch: (
request: Request,
Env?: E['Bindings'] | {},
executionCtx?: ExecutionContext
) => Response | Promise<Response> = (request, ...rest) => {
return this.#dispatch(request, rest[1], rest[0], request.method)
}ミドルウェアも同様に Response を返す関数として統一されている。CORS ミドルウェアの preflight 処理は new Response(null, { headers, status: 204 }) を直接返す(src/middleware/cors/index.ts:146-151)。HTTPException も getResponse() で new Response() を返す(src/http-exception.ts:67-77)。
Adapter パターンによるランタイム固有 API の隔離
ランタイムごとに異なる API(接続情報の取得、ファイルシステムアクセス、WebSocket)は src/adapter/ に隔離され、共通インターフェースで抽象化される。
// src/helper/conninfo/types.ts:45
export type GetConnInfo = (c: Context) => ConnInfo
// src/adapter/cloudflare-workers/conninfo.ts:3-7
export const getConnInfo: GetConnInfo = (c) => ({
remote: {
address: c.req.header('cf-connecting-ip'),
},
})
// src/adapter/bun/conninfo.ts:10-43
export const getConnInfo: GetConnInfo = (c: Context) => {
const server = getBunServer<{...}>(c)
const info = server.requestIP(c.req.raw)
return { remote: { address: info.address, ... } }
}
// src/adapter/deno/conninfo.ts:8-17
export const getConnInfo: GetConnInfo = (c) => {
const { remoteAddr } = c.env
return { remote: { address: remoteAddr.hostname, ... } }
}同じ GetConnInfo 型を満たしつつ、Cloudflare は HTTP ヘッダから、Bun は server.requestIP() から、Deno は c.env.remoteAddr から取得する。ユーザーコードは import { getConnInfo } from 'hono/cloudflare-workers' のように、ランタイムに応じたアダプタを import するだけでよい。
ホットパスでの標準 API 回避
getPath() は URL パースの最頻出処理であり、new URL() の代わりに文字列操作で実装している。
// src/utils/url.ts:106-134
export const getPath = (request: Request): string => {
const url = request.url;
const start = url.indexOf("/", url.indexOf(":") + 4);
let i = start;
for (; i < url.length; i++) {
const charCode = url.charCodeAt(i);
if (charCode === 37) { // '%'
// percent encoding がある場合のみ indexOf にフォールバック
const queryIndex = url.indexOf("?", i);
const hashIndex = url.indexOf("#", i);
// ...
} else if (charCode === 63 || charCode === 35) { // '?' or '#'
break;
}
}
return url.slice(start, i);
};charCode 比較によるバイト単位の走査、percent encoding がない場合(大多数のケース)は indexOf すら呼ばない最適化が施されている。同様に context.ts の text() メソッドもヘッダやステータスが未設定の場合は new Response(text) を直接返す fast path を持つ(src/context.ts:677-684)。
Object.create(null) による辞書の高速化
全ルーター実装で params や children の格納に Object.create(null) を使用している。
// src/router/reg-exp-router/router.ts:17
const nullMatcher: Matcher<any> = [/^$/, [], Object.create(null)];
// src/router/trie-router/node.ts:16
const emptyParams = Object.create(null);
// src/router/linear-router/router.ts:7
const emptyParams = Object.create(null);
// src/router/pattern-router/router.ts:6
const emptyParams = Object.create(null);通常のオブジェクトリテラル {} はプロトタイプチェーンを持つため、hasOwnProperty チェックが必要になり、V8 の hidden class 最適化にも影響する。Object.create(null) はプロトタイプを持たない純粋なハッシュマップとして機能する。
SmartRouter: 遅延評価と自己書き換えによる最適化
SmartRouter は最初の match() 呼び出し時に複数のルーター候補を試し、成功したルーターに自身の match メソッドを書き換える。
// src/router/smart-router/router.ts:21-60
match(method: string, path: string): Result<T> {
const routers = this.#routers
const routes = this.#routes
for (; i < len; i++) {
const router = routers[i]
try {
for (let i = 0, len = routes.length; i < len; i++) {
router.add(...routes[i])
}
res = router.match(method, path)
} catch (e) {
if (e instanceof UnsupportedPathError) {
continue // 次のルーターを試す
}
throw e
}
this.match = router.match.bind(router) // 自己書き換え
this.#routers = [router]
this.#routes = undefined // ルート定義を GC 可能に
break
}
// ...
}this.match = router.match.bind(router) で2回目以降は直接選択されたルーターの match を呼ぶ。Strategy パターンの動的選択だが、一度選択したら委譲コストもゼロになる点が特徴的。
プリセットによるバンドルサイズ制御
コアクラス HonoBase を共有しつつ、ルーターの組み合わせだけが異なるプリセットを提供する。
// src/hono.ts (default: 高速ルーティング重視)
export class Hono<...> extends HonoBase<...> {
constructor(options = {}) {
super(options)
this.router = options.router ??
new SmartRouter({ routers: [new RegExpRouter(), new TrieRouter()] })
}
}
// src/preset/tiny.ts (最小バンドルサイズ重視: < 12kB)
export class Hono<...> extends HonoBase<...> {
constructor(options = {}) {
super(options)
this.router = new PatternRouter()
}
}これにより、同じ API を維持したまま、エッジ環境(サイズ重視)とサーバー環境(速度重視)で最適な構成を選択できる。
パターンカタログ
Strategy パターン (分類: 振る舞い)
- 解決する問題: ルーティングアルゴリズムの交換可能性
- 適用条件: 同じインターフェース (
Router<T>) を満たす複数の実装が存在し、使用場面で最適な実装が異なる場合 - コード例:
src/router.ts:29-52(Router インターフェース),src/hono.ts:27-33(Strategy の注入) - 注意点: SmartRouter が初回リクエスト時に自動選択する変形で、一度選択されたら変更されない(Strategy の動的切り替えではなく、遅延バインディング)
Adapter パターン (分類: 構造)
- 解決する問題: ランタイム固有 API を共通インターフェースに変換
- 適用条件: 同じ概念(接続情報、ファイルアクセス等)がランタイムごとに異なる API で提供される場合
- コード例:
src/helper/conninfo/types.ts:45(共通型),src/adapter/*/conninfo.ts(各実装) - 注意点: Adapter は
src/adapter/に配置し、ユーザーが明示的に import する設計。自動検出ではない
Template Method パターン (分類: 振る舞い)
- 解決する問題: Lambda イベント処理の共通フローと差分の分離
- 適用条件: 処理フローは共通だが、各ステップの実装がイベント種別ごとに異なる場合
- コード例:
src/adapter/aws-lambda/handler.ts:268-328(EventProcessor 抽象クラス)
Good Patterns
- fetch シグネチャをフレームワークの境界契約にする:
(Request, env?, ctx?) => Response | Promise<Response>という Web Standards ベースのシグネチャをフレームワークのエントリポイントにすることで、どのランタイムでもexport default appで動作する。テストもapp.request('/path')で標準 Request/Response を使って実行できる(src/helper/testing/index.ts:16-27)。
// src/hono-base.ts:493-511
request = (
input: RequestInfo | URL,
requestInit?: RequestInit,
Env?: E["Bindings"] | {},
executionCtx?: ExecutionContext,
): Response | Promise<Response> => {
if (input instanceof Request) {
return this.fetch(requestInit ? new Request(input, requestInit) : input, Env, executionCtx);
}
input = input.toString();
return this.fetch(
new Request(
/^https?:\/\//.test(input) ? input : `http://localhost${mergePath("/", input)}`,
requestInit,
),
Env,
executionCtx,
);
};- ランタイム固有のコードを Adapter としてサブパスに分離する:
hono/cloudflare-workers,hono/bun,hono/denoなど、ランタイム固有のコードは package.json のexportsでサブパスとして公開する。コア (hono) はランタイム非依存を保ち、ユーザーは必要なアダプタだけを import する。ツリーシェイキングにも有利。
// package.json (抜粋)
"exports": {
".": { "import": "./dist/index.js" }, // コア (ランタイム非依存)
"./cloudflare-workers": { "import": "..." }, // CF Workers アダプタ
"./bun": { "import": "..." }, // Bun アダプタ
"./deno": { "import": "..." }, // Deno アダプタ
}- 単一ハンドラの場合に compose をスキップする最適化: ミドルウェアチェーンが1つしかない場合、
compose()を呼ばず直接ハンドラを実行する(src/hono-base.ts:424-442)。大多数のルートはミドルウェアなしか少数であるため、この最適化は全体のスループットに大きく寄与する。
Anti-Patterns / 注意点
- ホットパスでの new URL() 依存: URL パースに
new URL()を使うと、オブジェクト生成・プロトコル検証・ホスト解析などの不要な処理が毎リクエスト発生する。パスだけが必要な場面では文字列操作の方が桁違いに速い。
// Bad: 毎リクエストで URL オブジェクトを生成
const getPath = (request: Request): string => {
return new URL(request.url).pathname;
};
// Better: 文字列操作でパスだけを抽出 (Hono の実装)
const getPath = (request: Request): string => {
const url = request.url;
const start = url.indexOf("/", url.indexOf(":") + 4);
// charCode 比較で '?' や '#' を検出し slice で返す
};- 共通辞書での {} リテラル使用: ルートパラメータなどの頻繁にアクセスされるオブジェクトに
{}を使うと、プロトタイプチェーンの走査コストが発生し、__proto__やconstructorがキーとして衝突するリスクもある。
// Bad: プロトタイプチェーン付きオブジェクト
const params: Record<string, string> = {};
// Better: プロトタイプなしの純粋辞書
const params: Record<string, string> = Object.create(null);導出ルール
[MUST]マルチランタイム対応ライブラリでは、コアのエントリポイントを Web Standards API(Request/Response)で定義し、ランタイム固有 API はアダプタに隔離する- 根拠: Hono は
fetch(Request) => Responseを境界契約とし、src/adapter/に CF Workers / Bun / Deno / AWS Lambda 等の固有処理を隔離することで、コアコードの変更なしに9つ以上のランタイムをサポートしている
- 根拠: Hono は
[MUST]フレームワークやライブラリのコアモジュールは外部ランタイム依存(node:crypto,node:fs等)を含めず、Web Standards API(crypto.subtle,ReadableStream,TextEncoder等)のみを使用する- 根拠: Hono は
dependenciesがゼロで、暗号処理をcrypto.subtle(src/utils/crypto.ts:45-46)、ストリーミングをTransformStream(src/helper/streaming/stream.ts:12)で実装し、全ランタイムで動作を保証している
- 根拠: Hono は
[SHOULD]リクエスト処理のホットパスではnew URL()やURLSearchParamsを避け、文字列操作でパース処理を実装する- 根拠: Hono の
getPath()はcharCode比較による文字列走査で URL パスを抽出し(src/utils/url.ts:106-134)、getQueryParam()も手動パースしている(src/utils/url.ts:219-300)。percent encoding がないケース(大多数)ではindexOfすら呼ばない最適化が施されている
- 根拠: Hono の
[SHOULD]ホットパスでオブジェクトを辞書として使う場合はObject.create(null)でプロトタイプチェーンを排除する- 根拠: Hono の全ルーター実装(RegExpRouter, TrieRouter, LinearRouter, PatternRouter)で params, children, cache に
Object.create(null)を使用している(該当箇所 25 件以上)
- 根拠: Hono の全ルーター実装(RegExpRouter, TrieRouter, LinearRouter, PatternRouter)で params, children, cache に
[SHOULD]同じインターフェースの複数実装がある場合、デフォルトは自動選択(SmartRouter パターン)にし、上級ユーザーには明示的な選択を許可する- 根拠: SmartRouter は初回リクエスト時に最適なルーターを自動選択し、
this.matchを書き換えて以降のオーバーヘッドをゼロにする(src/router/smart-router/router.ts:46)。プリセット(hono/tiny,hono/quick)で明示的な選択も可能
- 根拠: SmartRouter は初回リクエスト時に最適なルーターを自動選択し、
[SHOULD]バンドルサイズが異なる複数のプリセットを提供する場合、コアクラスを共有し、差分(ルーター等)だけを差し替える構成にする- 根拠:
HonoBaseを共有し、Hono(デフォルト),hono/tiny(< 12kB),hono/quick(起動高速)がルーターの組み合わせだけ異なる(src/hono.ts,src/preset/tiny.ts,src/preset/quick.ts)
- 根拠:
[AVOID]ランタイム検出による分岐をコアコードに埋め込むこと。代わりに、ユーザーが import パスでランタイムを選択する設計にする- 根拠: Hono は
if (typeof Deno !== 'undefined')のようなランタイム検出をコアに持たず、hono/cloudflare-workers/hono/bun/hono/denoのサブパス import でアダプタを選択させる。これにより dead code elimination が効き、バンドルサイズが最小化される
- 根拠: Hono は
適用チェックリスト
- [ ] ライブラリのコアモジュールが
node:プレフィックス付きモジュールに依存していないか確認する - [ ] エントリポイントのシグネチャが Web Standards の
Request/Responseベースになっているか確認する - [ ] ランタイム固有のコードが
adapter/や専用サブパスに隔離されているか確認する - [ ] ホットパスで
new URL(),URLSearchParams,JSON.parse()を不必要に使用していないか確認する - [ ] 頻繁にアクセスされるオブジェクト辞書が
Object.create(null)で生成されているか確認する - [ ]
package.jsonのexportsでサブパスを定義し、ツリーシェイキングを有効にしているか確認する - [ ] デフォルト構成がほとんどのユーザーにとって最適か、上級ユーザー向けのカスタマイズ手段があるか確認する
- [ ] 単一ハンドラのような頻出ケースに fast path を用意しているか確認する