Hono ships five router implementations behind one small interface, and the default Hono class does not pick one -- it defers the choice to the first request. This page explains the interface, the selection mechanism, and what each router is for.

The contract

Every router implements two methods (src/router.ts):

src/router.ts
export interface Router<T> {
  name: string
  add(method: string, path: string, handler: T): void
  match(method: string, path: string): Result<T>
}

The Result shape is documented in the same file, and it explains a lot about Hono's performance model -- a match returns all handlers for the path (middleware included) plus parameters, either as indexes into a stash or as resolved strings:

src/router.ts
 * [[handler, paramIndexMap][], paramArray]
 * [
 *   [
 *     [middlewareA, {}],                     // '*'
 *     [funcA,       {'id': 0}],              // '/user/:id/*'
 *     [funcB,       {'id': 0, 'action': 1}], // '/user/:id/:action'
 *   ],
 *   ['123', 'abc']
 * ]

One match() call yields the entire execution plan for the request; nothing is matched again downstream.

SmartRouter: decide at first match

The default app wires two routers into a SmartRouter:

src/hono.ts
constructor(options: HonoOptions<E> = {}) {
  super(options)
  this.router =
    options.router ??
    new SmartRouter({
      routers: [new RegExpRouter(), new TrieRouter()],
    })
}

SmartRouter buffers add() calls, then on the first match() replays them into the first candidate router. If that router throws UnsupportedPathError (RegExpRouter cannot express some patterns), it falls through to the next. Whichever succeeds becomes the router permanently -- match is literally replaced with the winner's bound method:

src/router/smart-router/router.ts
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

After selection, SmartRouter has zero per-request overhead. The internal structure:

flowchart TD
    A["new Hono()"] --> B[SmartRouter]
    B -->|"try first"| C[RegExpRouter]
    B -->|"UnsupportedPathError fallback"| D[TrieRouter]
    Q["new Hono() from hono/quick"] --> E[SmartRouter]
    E -->|"try first"| F[LinearRouter]
    E -->|fallback| D2[TrieRouter]
    T["new Hono() from hono/tiny"] --> G[PatternRouter]

The five routers

Router Strategy Best for Limits
RegExpRouter Compiles all routes for a method into one big regular expression plus a static-path map Long-lived processes; fastest matching Throws UnsupportedPathError on patterns its trie compiler cannot express; matcher is frozen after first match
TrieRouter Classic prefix trie walked per request Universal fallback; supports every pattern Slower than the compiled regexp
LinearRouter Scans the route list on every request One-shot environments (per-request isolates) where registration speed beats match speed O(routes) per match
PatternRouter One small regexp per route Smallest code size (the hono/tiny preset) Linear scan over per-route regexps
SmartRouter Delegates to the first candidate that can hold the route table The default; picks RegExpRouter when possible Routes cannot be added after the first match (Can not add a route since the matcher is already built.)

Why RegExpRouter is fast

RegExpRouter.match first consults a plain-object map of static paths -- a static route never touches the regexp at all. Dynamic paths run one path.match(...) against the method's combined pattern, and the index of the first empty capture group selects the handler set:

src/router/reg-exp-router/matcher.ts
const staticMatch = matcher[2][path]
if (staticMatch) {
  return staticMatch
}

const match = path.match(matcher[0])
if (!match) {
  return [[], emptyParam]
}

const index = match.indexOf('', 1)
return [matcher[1][index], match]

Parameter values are not copied into objects during the match: the raw regexp match array itself is the ParamStash, and c.req.param() resolves names to stash indexes only when asked (see Context and HonoRequest).

All five implementations are held to the same behavior by a shared spec, src/router/common.case.test.ts, which runs the same route/match cases against each router.

Choosing one yourself

Pass any router via the router option (new Hono({ router: new RegExpRouter() })), or pick a preset entry point -- hono/quick and hono/tiny exist purely to pre-select routers for specific deployment shapes. Details in the exports reference.

Sources: src/router.ts, src/hono.ts, src/router/smart-router/router.ts, src/router/reg-exp-router/router.ts, src/router/trie-router/router.ts, src/router/linear-router/router.ts, src/router/pattern-router/router.ts · last synced 2026-08-10 · 26de731 · version 4.13.1