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):
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:
* [[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:
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:
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:
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.