Skip to main content
doc0blade47/honodocumentation home
Sign in

Trie and Smart Routers

Last updated Oct 12, 2026100% grounded
Relevant source files

The following files were used as context for generating this wiki page:

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 / MethodType / SignatureDescription
namestringIdentifies the router name, initialized as 'SmartRouter' and updated to include the active router name post-selection (e.g. SmartRouter + RegExpRouter).
#routersRouter<T>[]Internal array of candidate routing backends provided at instantiation, narrowing down to the single active router upon successful execution.
#routes[string, string, T][] | undefinedTemporary storage holding registered route tuples before matcher locking; set to undefined once a router successfully handles a match.
activeRouterRouter<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

Was this page helpful?