Relevant source files
The following files were used as context for generating this wiki page:
- src/router/smart-router/router.ts
- src/router/trie-router/node.ts
- src/router/trie-router/index.ts
- src/helper/route/index.ts
- src/router/trie-router/router.ts
- src/types.ts
- src/preset/quick.ts
- src/router/reg-exp-router/prepared-router.ts
- src/router/linear-router/router.ts
- src/router/reg-exp-router/router.ts
- src/router/reg-exp-router/node.ts
- src/middleware/ip-restriction/index.ts
- src/hono.ts
- src/router/reg-exp-router/trie.ts
- src/router/reg-exp-router/matcher.ts
- src/middleware/language/language.ts
- src/utils/url.ts
- src/router/smart-router/index.ts
- src/helper/dev/index.ts
- src/router/pattern-router/router.ts
- src/hono-base.ts
- src/utils/ipaddr.ts
- package.json
- src/preset/tiny.ts
- src/router.ts
- src/router/linear-router/index.ts
- src/helper/ssg/utils.ts
- src/helper/adapter/index.ts
- src/router/reg-exp-router/index.ts
- src/router/pattern-router/index.ts
Overview
Hono's TrieRouter and SmartRouter deliver hierarchical prefix routing and dynamic dispatch across multiple matching backends. TrieRouter manages route registration through tree node insertion, supporting optional parameter expansion, path segment tokenization, and recursive handler set collection. To combine different routing strategies, SmartRouter aggregates multiple router instances, delegating registration to each candidate until a successful match occurs and locking onto the winning backend for subsequent requests. Sources: src/router/smart-router/router.ts:4-70, src/router/trie-router/node.ts:25-112, src/router/trie-router/router.ts:5-27
TrieRouter Architecture and Route Registration
Public Interface and Root Node Management
TrieRouter<T> implements the unified Router<T> interface, identifying itself via the name property set to 'TrieRouter'. The class initializes a private root #node instance of type Node<T> inside its constructor to anchor the entire route prefix tree.
export class TrieRouter<T> implements Router<T> {
name: string = 'TrieRouter'
#node: Node<T>
constructor() {
this.#node = new Node()
}
...Sources: src/router/trie-router/router.ts:5-11
Optional Parameter Expansion and Route Insertion
When adding routes through the add(method, path, handler) method, TrieRouter inspects the incoming route path using checkOptionalParameter(path). If optional parameters are present, the utility returns an array of expanded path variations; the router iterates over these results and inserts each individual variation into the root node. If no optional parameters are detected, the path is inserted directly.
add(method: string, path: string, handler: T) {
const results = checkOptionalParameter(path)
if (results) {
for (let i = 0, len = results.length; i < len; i++) {
this.#node.insert(method, results[i], handler)
}
return
}
this.#node.insert(method, path, handler)
}Sources: src/router/trie-router/router.ts:13-23
Note
checkOptionalParameter handles path patterns containing optional segments by generating all possible route combinations, ensuring each variant is separately registered in the prefix tree.
Sources: src/router/trie-router/router.ts:14-20
Route Matching Delegation
Incoming request matching delegates execution directly to the root node's search method, forwarding both the HTTP method and request path and returning a resolved Result<T> structure containing matched handlers and captured parameters.
match(method: string, path: string): Result<T> {
return this.#node.search(method, path)
}Sources: src/router/trie-router/router.ts:25-27
Trie Node Structure and Insertion
Node Fields and Constructor Initialization
The Node<T> class maintains the internal state of the prefix tree using private fields. Each node tracks its registered methods, child nodes, extracted routing patterns, insertion order counters, and parameter records.
export class Node<T> {
#methods: Record<string, HandlerSet<T>>[]
#children: Record<string, Node<T>>
#patterns: Pattern[]
#order: number = 0
#params: Record<string, string> = emptyParams
constructor(method?: string, handler?: T, children?: Record<string, Node<T>>) {
this.#children = children || Object.create(null)
this.#methods = []
if (method && handler) {
const m: Record<string, HandlerSet<T>> = Object.create(null)
m[method] = { handler, possibleKeys: [], score: 0 }
this.#methods = [m]
}
this.#patterns = []
}Sources: src/router/trie-router/node.ts:25-42
Note
emptyParams is initialized via Object.create(null) to provide a prototype-free parameter dictionary.
Sources: src/router/trie-router/node.ts:16-16
Path Segment Tokenization and Pattern Extraction
During route insertion, insert(method, path, handler) increments the node order counter and splits the routing path into individual segments using splitRoutingPath(path). It iterates through each segment, calling getPattern(p, nextP) to extract parameter and wildcard patterns.
insert(method: string, path: string, handler: T): Node<T> {
this.#order = ++this.#order
// eslint-disable-next-line @typescript-eslint/no-this-alias
let curNode: Node<T> = this
const parts = splitRoutingPath(path)
const possibleKeys: string[] = []
for (let i = 0, len = parts.length; i < len; i++) {
const p: string = parts[i]
const nextP = parts[i + 1]
const pattern = getPattern(p, nextP)
const key = Array.isArray(pattern) ? pattern[0] : p
if (key in curNode.#children) {
curNode = curNode.#children[key]
if (pattern) {
possibleKeys.push(pattern[1])
}
continue
}Sources: src/router/trie-router/node.ts:44-65
Recursive Tree Construction and Handler Binding
If a segment key already exists within curNode.#children, traversal descends into that child node. Otherwise, a new Node instance is instantiated and registered. Extracted patterns are pushed to #patterns, and final handler sets are appended to #methods with deduplicated possibleKeys and an insertion order score.
curNode.#children[key] = new Node()
if (pattern) {
curNode.#patterns.push(pattern)
possibleKeys.push(pattern[1])
}
curNode = curNode.#children[key]
}
curNode.#methods.push({
[method]: {
handler,
possibleKeys: possibleKeys.filter((v, i, a) => a.indexOf(v) === i),
score: this.#order,
},
})
return curNode
}Sources: src/router/trie-router/node.ts:66-85
Warning
Handler sets filter possibleKeys using a.indexOf(v) === i to prevent duplicate parameter keys within the same handler registration block.
Sources: src/router/trie-router/node.ts:76-82
Hierarchical Path Traversal and Matching
Recursive Search Execution and Queue Management
Path lookup begins by invoking search(method, path) on the root node, which splits the request path using splitPath(path) and initializes a traversal queue of active nodes (curNodes) alongside curNodesQueue for handling multi-segment regular expression parameter matches.
search(method: string, path: string): [[T, Params][]] {
const handlerSets: HandlerParamsSet<T>[] = []
this.#params = emptyParams
// eslint-disable-next-line @typescript-eslint/no-this-alias
const curNode: Node<T> = this
let curNodes = [curNode]
const parts = splitPath(path)
const curNodesQueue: Node<T>[][] = []
const len = parts.length
let partOffsets: number[] | null = null
Sources: src/router/trie-router/node.ts:114-126
Static Child Lookup and Wildcard Handling
During iteration over path segments, the router checks static child nodes via node.#children[part]. When reaching the final path segment on a matching child, it evaluates trailing wildcard children (nextNode.#children['*']) and pushes matching handler sets into the accumulation array.
for (let i = 0; i < len; i++) {
const part: string = parts[i]
const isLast = i === len - 1
const tempNodes: Node<T>[] = []
for (let j = 0, len2 = curNodes.length; j < len2; j++) {
const node = curNodes[j]
const nextNode = node.#children[part]
if (nextNode) {
nextNode.#params = node.#params
if (isLast) {
// '/hello/*' => match '/hello'
if (nextNode.#children['*']) {
this.#pushHandlerSets(handlerSets, nextNode.#children['*'], method, node.#params)
}
this.#pushHandlerSets(handlerSets, nextNode, method, node.#params)
} else {
tempNodes.push(nextNode)
}
}
Sources: src/router/trie-router/node.ts:127-148
Note
Wildcard patterns (*) directly inspect child nodes keyed by *, pushing their associated handler sets and enqueuing the wildcard astNode for subsequent path segments.
Sources: src/router/trie-router/node.ts:153-163
Parameter Capture and Regular Expression Matchers
Nodes with patterns evaluate parameters and regular expressions against the remaining path string using precomputed part offsets. If a regular expression matcher succeeds, captured values populate the parameters dictionary and child nodes are scheduled via curNodesQueue.
const [key, name, matcher] = pattern
if (!part && !(matcher instanceof RegExp)) {
continue
}
const child = node.#children[key]
// `/js/:filename{[a-z]+.js}` => match /js/chunk/123.js
if (matcher instanceof RegExp) {
if (partOffsets === null) {
partOffsets = new Array(len)
let offset = path[0] === '/' ? 1 : 0
for (let p = 0; p < len; p++) {
partOffsets[p] = offset
offset += parts[p].length + 1
}
}
const restPathString = path.substring(partOffsets[i])
const m = matcher.exec(restPathString)
if (m) {
params[name] = m[0]
this.#pushHandlerSets(handlerSets, child, method, node.#params, params)
if (hasChildren(child.#children)) {
child.#params = params
const componentCount = m[0].match(/\//)?.length ?? 0
const targetCurNodes = (curNodesQueue[componentCount] ||= [])
targetCurNodes.push(child)
}
continue
}
}
if (matcher === true || matcher.test(part)) {
params[name] = part
if (isLast) {Sources: src/router/trie-router/node.ts:165-203
Handler Set Collection
The #pushHandlerSets helper method resolves method-specific handlers or METHOD_NAME_ALL fallbacks, binds parameter dictionaries from possibleKeys, and populates the output handlerSets collection.
#pushHandlerSets(
handlerSets: HandlerParamsSet<T>[],
node: Node<T>,
method: string,
nodeParams: Record<string, string>,
params?: Record<string, string>
): void {
for (let i = 0, len = node.#methods.length; i < len; i++) {
const m = node.#methods[i]
const handlerSet = (m[method] || m[METHOD_NAME_ALL]) as HandlerParamsSet<T>
const processedSet: Record<number, boolean> = {}
if (handlerSet !== undefined) {
handlerSet.params = Object.create(null)
handlerSets.push(handlerSet)
if (nodeParams !== emptyParams || (params && params !== emptyParams)) {
for (let i = 0, len = handlerSet.possibleKeys.length; i < len; i++) {
const key = handlerSet.possibleKeys[i]
const processed = processedSet[handlerSet.score]
handlerSet.params[key] =
params?.[key] && !processed ? params[key] : (nodeParams[key] ?? params?.[key])
processedSet[handlerSet.score] = true
}
}
}
}
}Sources: src/router/trie-router/node.ts:87-112
Warning
Parameter inheritance checks nodeParams against params using processedSet deduplication flags to ensure route parameters maintain correct scope across hierarchical child transitions.
Sources: src/router/trie-router/node.ts:97-110
SmartRouter Architecture and Routing Backends
Multi-Router Aggregation and Registration Contract
The SmartRouter<T> class implements the unified Router<T> interface by aggregating an array of candidate router backends via its constructor initialization parameter (init.routers). During the registration phase, calling add(method, path, handler) pushes route tuples [method, path, handler] into the internal #routes collection. If route addition is attempted after the matcher has already been built, it throws an error with the message defined by MESSAGE_MATCHER_IS_ALREADY_BUILT.
export class SmartRouter<T> implements Router<T> {
name: string = 'SmartRouter'
#routers: Router<T>[] = []
#routes?: [string, string, T][] = []
constructor(init: { routers: Router<T>[] }) {
this.#routers = init.routers
}
add(method: string, path: string, handler: T) {
if (!this.#routes) {
throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT)
}
this.#routes.push([method, path, handler])
}
Sources: src/router/smart-router/router.ts:4-20
Warning
Calling add() after matching has been evaluated throws an error because #routes is set to undefined once an active router locks in.
Sources: src/router/smart-router/router.ts:13-19, src/router/smart-router/router.ts:48-48
Routing Delegation and Matcher Locking
When match(method, path) is invoked, SmartRouter iterates through the candidate routers in #routers, populating each candidate with all accumulated routes from #routes and testing them sequentially. If an UnsupportedPathError occurs on a candidate router, the loop catches the exception, ignores it with continue, and falls back to the next available router backend.
match(method: string, path: string): Result<T> {
if (!this.#routes) {
throw new Error('Fatal error')
}
const routers = this.#routers
const routes = this.#routes
const len = routers.length
let i = 0
let res
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
break
}
Sources: src/router/smart-router/router.ts:21-51
Tip
Upon a successful match, SmartRouter mutates its own prototype methods and state by binding this.match directly to the winning router.match, reducing #routers to a single-element array containing the active router, and clearing #routes.
Sources: src/router/smart-router/router.ts:46-49
Router State Properties
| Property / Method | Type / Signature | Description |
|---|---|---|
name | string | Identifies the router name, initialized as 'SmartRouter' and updated to include the active router name post-selection (e.g. SmartRouter + RegExpRouter). |
#routers | Router<T>[] | Internal array of candidate routing backends provided at instantiation, narrowing down to the single active router upon successful execution. |
#routes | [string, string, T][] | undefined | Temporary storage holding registered route tuples before matcher locking; set to undefined once a router successfully handles a match. |
activeRouter | Router<T> | Getter returning the single active router instance once routing has locked in, throwing an error if routes are still pending or multiple routers remain. |
export class SmartRouter<T> implements Router<T> {
name: string = 'SmartRouter'
#routers: Router<T>[] = []
#routes?: [string, string, T][] = []
constructor(init: { routers: Router<T>[] }) {
this.#routers = init.routers
}Sources: src/router/smart-router/router.ts:4-11, src/router/smart-router/router.ts:63-69
Dynamic Router Selection and Fallback
Heuristic Selection on First Match
SmartRouter resolves which underlying router backend handles incoming traffic dynamically during the initial call to match(method, path). It iterates through each candidate router in #routers, replaying all buffered route tuples from #routes into the candidate via its add() method, and immediately attempts to match the target request. If a candidate router successfully executes the match without throwing an UnsupportedPathError or other exceptions, selection halts immediately via a break statement.
const routers = this.#routers
const routes = this.#routes
const len = routers.length
let i = 0
let res
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
break
}Sources: src/router/smart-router/router.ts:26-50
Warning
If all candidate routers fail to match or throw unhandled exceptions, the outer loop completes without breaking (i === len), triggering a fatal error throw that halts execution.
Sources: src/router/smart-router/router.ts:52-55
UnsupportedPathError Handling and Matcher Locking
During heuristic evaluation, candidate routers may encounter path syntax or expression structures they cannot process, throwing an UnsupportedPathError. The try...catch block inspects caught errors; if e instanceof UnsupportedPathError is true, the error is suppressed, and execution continues to the next candidate router in the sequence. Any other error type is rethrown immediately.
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
}Sources: src/router/smart-router/router.ts:34-44
Tip
Once a candidate successfully matches, SmartRouter locks its matcher by reassigning this.match to bind directly to the winning router.match, collapsing #routers to a single-element array containing only the active router, setting #routes to undefined, and updating this.name to reflect the active backend (e.g., SmartRouter + RegExpRouter). Subsequent requests bypass heuristic iteration entirely.
Sources: src/router/smart-router/router.ts:46-58
Related
On This Page
OverviewTrieRouter Architecture and Route RegistrationPublic Interface and Root Node ManagementOptional Parameter Expansion and Route InsertionRoute Matching DelegationTrie Node Structure and InsertionNode Fields and Constructor InitializationPath Segment Tokenization and Pattern ExtractionRecursive Tree Construction and Handler BindingHierarchical Path Traversal and MatchingRecursive Search Execution and Queue ManagementStatic Child Lookup and Wildcard HandlingParameter Capture and Regular Expression MatchersHandler Set CollectionSmartRouter Architecture and Routing BackendsMulti-Router Aggregation and Registration ContractRouting Delegation and Matcher LockingRouter State PropertiesDynamic Router Selection and FallbackHeuristic Selection on First MatchUnsupportedPathError Handling and Matcher LockingRelated