Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ROUTERCENTRISM: ルーティングに還元されるすべてについて

Avatar for Taku Amano Taku Amano
October 11, 2026

ROUTERCENTRISM: ルーティングに還元されるすべてについて

Avatar for Taku Amano

Taku Amano

October 11, 2026

More Decks by Taku Amano

Other Decks in Technology

Transcript

  1. 2

  2. 3

  3. Eight Routers 1. TrieRouter 基準 2. RegExpRouter 速い 3. StaticRouter

    吸収された 4. PatternRouter 小さい 13 5. LinearRouter 1shot が速い 6. SmartRouter 組み合わせ可 7. PreparedRegExpRouter hono/tiny 8. TransformRouter New! hono/quick 準備したら速い
  4. ルーターの interface export interface Router<T> { name: string add(method: string,

    path: string, handler: T): void match(method: string, path: string): Result<T> } 14
  5. ルーターの interface : add() export interface Router<T> { name: string

    add(method: string, path: string, handler: T): void match(method: string, path: string): Result<T> } add('ALL', '*', logger) 登録 add('GET', '/users/:id', handler) 15 Router
  6. ルーターの interface : match() export interface Router<T> { name: string

    add(method: string, path: string, handler: T): void match(method: string, path: string): Result<T> } GET /users/1 16 match('GET', '/users/1') Router 束 logger → handler
  7. ルーターの役割 match() に method と path を与えた時に、handler の束( Handler[] )を返すのが

    ルーターの役割 path の一致で柔軟にルーティングできる 17
  8. パスの代わりに、handler にマッチさせる「条件」を登録する import { JevRouter } from 'hono-jev-router' const app

    = new Hono({ router: new JevRouter({ apiKey }) }) const md = { 'Content-Type': 'text/markdown' } app.on('jev', 'a request from an AI agent', (c) => c.text('# Documentation', 200, md)) app.on('jev', 'a request from a human browser', (c) => c.html('<h1>Documentation</h1>')) app.on('jev', 'suspicious automated traffic', (c) => c.text('Forbidden', 403)) README の例。Jev へ「リクエストの情報」と「条件」を使って問い合わせて、handler を選択する 19
  9. 使い方 import { RegExpRouter } from 'hono/router/reg-exp-router' import { TransformRouter

    } from 'hono/router/transform-router' const app = new Hono({ router: new TransformRouter({ delegateRouter: new RegExpRouter(), transform: ({ handler, method, path }) => async (c, next) => { console.time(`${method} ${path}`) try { return await handler(c, next) } finally { console.timeEnd(`${method} ${path}`) } }, }), }) 26
  10. 実装 class TransformRouter implements Router<[H, RouterRoute]> { add(method: string, path:

    string, entry: [H, RouterRoute]) { const [handler, route] = entry this.#delegateRouter.add(method, path, [ this.#transform({ ...route, handler }), route, ]) } match(method: string, path: string) { return this.#delegateRouter.match(method, path) } } 27
  11. 例1: diagnostics_channel でトレースする import { tracingChannel } from 'node:diagnostics_channel' const

    tc = tracingChannel('hono:request') tc.subscribe({ start: (m) => console.log('start', m.route.path), asyncEnd: (m) => console.log('end', m.route.path), }) const router = new TransformRouter({ delegateRouter: new RegExpRouter(), transform: (route) => (c, next) => tc.tracePromise(() => route.handler(c, next), { route, c }), }) 29
  12. 例2: どの handler が投げたかを onError の前に知る const router = new

    TransformRouter({ delegateRouter: new RegExpRouter(), transform: ({ handler, method, path }) => async (c, next) => { try { return await handler(c, next) } catch (err) { console.error(`${method} ${path} threw`, err) throw err } }, }) app.onError((err, c) => c.text(err.message, 500)) 30
  13. 本体の実装に影響を与えることなく実現できる new Hono() の router オプションに、TransformRouter を渡すだけ TransformRouter を使わない既存のアプリには影響がない 変換は登録時に

    1 回だけ行われる リクエスト時のオーバーヘッドがない app.routes には元の handler が保持される transform された結果は TransformRouter 内で使用されるだけ showRoutes() の結果にも影響がない 31
  14. 登録時に、個別のルートに必要な束は決められる const app = new Hono() app.all('*', logger) app.all('/api/*', cors)

    app.get('/api/users/:id', handler) app.all('/api/*', fallback) * logger /api/* logger → cors /api/users/:id logger → cors → handler → fallback 33
  15. ルーター毎の違い 起動 リクエストが来る TrieRouter・他 登録 RegExpRouter 登録 PreparedRegExpRouter 畳む 登録

    取り出す 畳む 束を作る 実行 束を作る 束を 取り出す 実行 束を作る 束を 取り出す 実行 束 = middleware と handler の配列。Prepared- は「畳む」だけを起動前にツールで準備しておく。 34
  16. 修正前は、途中の * の扱いがルーターごとに違った ルーター /x*/y ← /xz/y /x*/y ← /x/y

    /a/b*/c ← /a/bz/c RegExp match — match Trie — — — Linear match match match Pattern — — — 39
  17. 8888 8888 修正後 ルーター /x*/y ← /xz/y /x*/y ← /x/y

    /a/b*/c ← /a/bz/c RegExp match match match Trie match match match Linear match match match Pattern match match match 40 88 88 88
  18. notFound/onError にも middleware を… 404 ページやエラーページをレンダリングする際に、middleware を適用したい 例: languageDetector const

    app = new Hono() app.notFound( languageDetector(), (c) => { return c.html(render404Page(c.get('language'))) } ) 43
  19. v4 ではルーティングの対象ではない 設定できるのはアプリに対して 1 つで、パスによる絞り込みはない middleware を利用できない onError() は subApp

    に対応している subApp の handler で発生したエラーは、subApp の onError() で処理できる notFound() は subApp には未対応 const app = new Hono() app.notFound((c) => c.text('Custom 404 Message', 404)) app.onError((err, c) => c.text('Custom Error Message', 500)) 45
  20. 予約メソッドにルートを登録する実装 notFound() は @NOT_FOUND 、 onError() は @ERROR @ 始まりは予約扱いにして、

    inspectRoutes / showRoutes などでは除外 const METHOD_NAME_NOT_FOUND = '@NOT_FOUND' const METHOD_NAME_ERROR = '@ERROR' this.onError = (...handlers: (string | H)[]) => this.#addRoutes(METHOD_NAME_ERROR, handlers) as any this.notFound = (...handlers: (string | H)[]) => this.#addRoutes(METHOD_NAME_NOT_FOUND, handlers) as any 47
  21. 例: subApp と app の両方に notFound を登録した場合 const app =

    new Hono() .get('/', home) .get('/health', healthCheck) const subApp = new Hono() .get('/users', listUsers) .get('/users/:id', getUser) .post('/users', createUser) .onError(handleApiError) .notFound(handleApiNotFound) app.route('/api', subApp) .onError(handleAppError) .notFound(handleAppNotFound) 52
  22. showRoutes で、エラーと 404 の handler も一覧に出せる showRoutes(app, { includeInternal: true,

    verbose: true }) GET GET GET GET POST @ERROR @NOT_FOUND @ERROR @NOT_FOUND 53 / /health /api/users /api/users/:id /api/users /api/* /api/* /* /* home healthCheck listUsers getUser createUser handleApiError handleApiNotFound handleAppError handleAppNotFound
  23. 例: /api 以下のエラー用 handler に処理を追加する const router = new TransformRouter({

    delegateRouter: new RegExpRouter(), transform: ({ handler, method, path }) => { if (method === '@ERROR' && path.startsWith('/api/')) { return async (c, next) => { await report(path, c.error) return handler(c, next) } } return handler }, }) app.onError('/api/*', (c) => c.text('api error', 500)) 55
  24. 通常のルーティングのままの場合 登録順だと、親の onError() で登録した handler でレスポンスが返ってしまう subApp の onError() は呼ばれない

    // '/*' app.onError(async (c) => c.text(`app: ${c.error?.message}`, 500)) // '/sub/*' subApp.onError(async (c, next) => c.text(`subApp: ${c.error?.message}`, 500)) app.route('/sub', subApp) 57
  25. 期待通りの順序になるようにルールを調整 各ルートは route() で追加された回数を depth として持つ マッチした handler は depth

    の深い順に並べる // '/*', depth: 0 app.onError(async (c) => c.text(`app: ${c.error?.message}`, 500)) // '/sub/*', depth: 1 subApp.onError(async (c, next) => c.text(`subApp: ${c.error?.message}`, 500)) app.route('/sub', subApp) 58