Skip to main content
doc0blade47/honodocumentation home
Sign in

RegExp Router

Last updated Oct 12, 2026100% grounded
Relevant source files

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

Overview

The RegExpRouter implements the unified Router Contracts interface by compiling registered path structures and HTTP method sets into a single optimized regular expression src/router/reg-exp-router/router.ts:122-207. Operating as a core backend option for Application Core via SmartRouter, it transforms wildcard patterns, path parameters, and static segments into a radix-style node trie that generates a unified matcher src/router/reg-exp-router/router.ts:34-103, src/router/reg-exp-router/trie.ts:6-73. By consolidating route evaluation, it avoids linear array scans during request dispatching src/router/reg-exp-router/matcher.ts:10-33. Additionally, PreparedRegExpRouter supports ahead-of-time serialization and pre-compilation patterns to eliminate runtime startup overhead src/router/reg-exp-router/prepared-router.ts:9-164.

Sources: src/router/reg-exp-router/router.ts:34-207, src/router/reg-exp-router/trie.ts:6-73, src/router/reg-exp-router/matcher.ts:10-33, src/router/reg-exp-router/prepared-router.ts:9-164

Architecture and Routing Model

Public Router Implementation and State Management

The RegExpRouter class implements the Router Contracts interface, managing route registration and request matching state through private collections src/router/reg-exp-router/router.ts:122-130. During the initialization phase, the router constructs separate storage maps for middleware and terminal routes under the ALL HTTP method (METHOD_NAME_ALL) src/router/reg-exp-router/router.ts:127-130.

export class RegExpRouter<T> implements Router<T> {
  name: string = 'RegExpRouter'
  #middleware?: Record<string, Record<string, HandlerWithMetadata<T>[]>>
  #routes?: Record<string, Record<string, HandlerWithMetadata<T>[]>>
 
  constructor() {
    this.#middleware = { [METHOD_NAME_ALL]: Object.create(null) }
    this.#routes = { [METHOD_NAME_ALL]: Object.create(null) }
  }
  // ...
}

Sources: src/router/reg-exp-router/router.ts:122-130

Important

Once a match operation compiles the underlying regular expression, the #middleware and #routes internal maps become undefined. Any subsequent call to add() after building will throw an error with MESSAGE_MATCHER_IS_ALREADY_BUILT.

Sources: src/router/reg-exp-router/router.ts:136-138

Route Registration Lifecycle

The add() method populates internal HTTP method tables, handles optional parameters via checkOptionalParameter, and processes wildcard suffix patterns src/router/reg-exp-router/router.ts:132-204. When a new HTTP method is encountered for the first time, the router initializes its maps by copying existing entries registered under ALL src/router/reg-exp-router/router.ts:140-147.

Internal Property / ParameterTypePurpose
namestringIdentifies the router backend as 'RegExpRouter'.
#middlewareRecord<string, Record<string, HandlerWithMetadata<T>[]>>Stores registered middleware paths grouped by HTTP method.
#routesRecord<string, Record<string, HandlerWithMetadata<T>[]>>Stores terminal route handlers grouped by HTTP method.
METHOD_NAME_ALLstringConstant designating catch-all HTTP methods ('ALL').
nullMatcherMatcher<any>Fallback matcher for empty routing tables ([/^$/, [], Object.create(null)]).

Sources: src/router.ts:9, src/router/reg-exp-router/router.ts:17, src/router/reg-exp-router/router.ts:122-130, src/router/reg-exp-router/router.ts:140-147

Wildcard patterns ending in * are expanded and tested against existing route entries using cached regular expressions built via buildWildcardRegExp src/router/reg-exp-router/router.ts:20-28, src/router/reg-exp-router/router.ts:155-186.

function buildWildcardRegExp(path: string): RegExp {
  return (wildcardRegExpCache[path] ??= new RegExp(
    path === '*'
      ? ''
      : `^${path.replace(/\/\*$|([.\\+*[^\]$()])/g, (_, metaChar) =>
          metaChar ? `\\${metaChar}` : '(?:|/.*)'
        )}$`
  ))
}

Sources: src/router/reg-exp-router/router.ts:20-28

Matcher Compilation Workflow

When request matching occurs, buildMatcherFromPreprocessedRoutes sorts paths, separates static and dynamic segments, and delegates structural insertion to a Trie instance src/router/reg-exp-router/router.ts:34-82. Static paths are stored directly in a staticMap for O(1) lookups, while dynamic paths contribute to a consolidated regular expression src/router/reg-exp-router/router.ts:43-82.

function buildMatcherFromPreprocessedRoutes<T>(
  routes: [string, HandlerWithMetadata<T>[]][]
): Matcher<T> {
  const trie = new Trie()
  const handlerData: HandlerData<T>[] = []
  if (routes.length === 0) {
    return nullMatcher
  }

Sources: src/router/reg-exp-router/router.ts:34-42

Warning

Path insertions that conflict with structural constraints or invalid syntax throw a PATH_ERROR, which the router catches and re-throws as an UnsupportedPathError.

Sources: src/router/reg-exp-router/router.ts:61-65

Trie Intermediate Representation

Path Tokenization and Group Preservation

The Trie class manages routing structures through a root Node instance and a shared context tracking variable indices (varIndex) src/router/reg-exp-router/trie.ts:6-8. When a route path is inserted, the insert() method first isolates bracketed custom regular expression groups (such as :id{[0-9]+}) by replacing them with temporary placeholder markers (@\i) src/router/reg-exp-router/trie.ts:13-26. It then tokenizes the modified path using a regular expression that matches named parameters (:[^\/]+), wildcard suffixes (/\*$), or individual characters, before restoring the original bracketed expressions into the correct token positions src/router/reg-exp-router/trie.ts:33-42.

    const groups: [string, string][] = [] // [mark, original string]
    for (let i = 0; ; ) {
      let replaced = false
      path = path.replace(/\{[^}]+\}/g, (m) => {
        const mark = `@\\${i}`
        groups[i] = [mark, m]
        i++
        replaced = true
        return mark
      })
      if (!replaced) {
        break
      }
    }

Sources: src/router/reg-exp-router/trie.ts:13-26

Node Graph Insertion and Conflict Checks

The Node class recursively inserts token arrays into its #children record map src/router/reg-exp-router/node.ts:45-133. During insertion, tokens are evaluated to determine their pattern types—distinguishing between standalone wildcards (*), tail wildcards (/*), named parameters with custom expressions, common label patterns ([^/]+), and literal characters src/router/reg-exp-router/node.ts:69-78.

Token / Pattern TypeRegular Expression StringAssigned Variable / Purpose
* (terminal).* (ONLY_WILDCARD_REG_EXP_STR)Matches all trailing paths without capturing a parameter name.
* (non-terminal)[^/]+ (LABEL_REG_EXP_STR)Acts as a generic path segment label.
/*`(?:/.*) (TAIL_WILDCARD_REG_EXP_STR`)
:label[^/]+ (LABEL_REG_EXP_STR)Captures standard path segments into paramMap.
:label{expr}expr (custom regex string)Validates and captures custom regex patterns; enforces non-capturing group constraints.

Sources: src/router/reg-exp-router/node.ts:1-3, src/router/reg-exp-router/node.ts:69-78

Caution

Inserting conflicting segment types into the same Trie node level—such as mixing literal characters with multi-character wildcards or mixing distinct regular expression patterns—immediately throws PATH_ERROR. Custom expressions containing capturing groups without non-capturing prefixes ((?!:\?)) are also rejected.

Sources: src/router/reg-exp-router/node.ts:87-91, src/router/reg-exp-router/node.ts:96-102, src/router/reg-exp-router/node.ts:117-124

Pattern Compilation and Radix Merging

Recursive Regex String Synthesis and Child Key Sorting

The Node class converts the hierarchical trie structure into a unified regular expression string via the buildRegExpStr() method src/router/reg-exp-router/node.ts:135-161. Child node keys are retrieved, sorted using the compareKey function, and mapped into sub-expressions src/router/reg-exp-router/node.ts:136-147.

function compareKey(a: string, b: string): number {
  if (a.length === 1) {
    return b.length === 1 ? (a < b ? -1 : 1) : -1
  }
  if (b.length === 1) {
    return 1
  }
 
  // wildcard
  if (a === ONLY_WILDCARD_REG_EXP_STR || a === TAIL_WILDCARD_REG_EXP_STR) {
    return 1
  } else if (b === ONLY_WILDCARD_REG_EXP_STR || b === TAIL_WILDCARD_REG_EXP_STR) {
    return -1
  }
 
  // label
  if (a === LABEL_REG_EXP_STR) {
    return 1
  } else if (b === LABEL_REG_EXP_STR) {
    return -1
  }
 
  return a.length === b.length ? (a < b ? -1 : 1) : b.length - a.length
}

Sources: src/router/reg-exp-router/node.ts:20-43

The sorting algorithm establishes strict evaluation precedence by organizing node keys in the following order: literal strings, special custom patterns, common label patterns ([^/]+), and wildcards (.* or (?:|/.*)) src/router/reg-exp-router/node.ts:14-19.

buildRegExpStr(): string {
  const childKeys = Object.keys(this.#children).sort(compareKey)
 
  const strList = childKeys.map((k) => {
    const c = this.#children[k]
    return (
      (typeof c.#varIndex === 'number'
        ? `(${k})@${c.#varIndex}`
        : regExpMetaChars.has(k)
          ? `\\${k}`
          : k) + c.buildRegExpStr()
    )
  })
 
  if (typeof this.#index === 'number') {
    strList.unshift(`#${this.#index}`)
  }
 
  if (strList.length === 0) {
    return ''
  }
  if (strList.length === 1) {
    return strList[0]
  }
 
  return '(?:' + strList.join('|') + ')'
}

Sources: src/router/reg-exp-router/node.ts:135-161

Note

When multiple child nodes exist at the same level, buildRegExpStr() joins them with alternation operators (|) wrapped in non-capturing groups (?:...). If a node marks a route termination point (#index), its handler identifier is prepended to the string list before alternation synthesis src/router/reg-exp-router/node.ts:149-160.

Sources: src/router/reg-exp-router/node.ts:149-160

Wildcard Normalization and Root Compilation

Wildcard routes and trailing patterns undergo specific normalization before and during trie integration src/router/reg-exp-router/router.ts:20-28. The router maintains a wildcardRegExpCache to compile wildcard expressions, translating /* suffixes into safe literal escape sequences combined with (?:|/.*) optional path matchers src/router/reg-exp-router/router.ts:19-28.

let wildcardRegExpCache: Record<string, RegExp> = Object.create(null)
function buildWildcardRegExp(path: string): RegExp {
  return (wildcardRegExpCache[path] ??= new RegExp(
    path === '*'
      ? ''
      : `^${path.replace(/\/\*$|([.\\+*[^\]$()])/g, (_, metaChar) =>
          metaChar ? `\\${metaChar}` : '(?:|/.*)'
        )}$`
  ))
}

Sources: src/router/reg-exp-router/router.ts:19-28

Once the trie graph is fully populated, Trie.buildRegExp() finalizes the compiled string by processing handler markers (#\d+), parameter markers (@\d+), and wildcard endings (\.\*\$) src/router/reg-exp-router/trie.ts:49-73. It constructs indexReplacementMap and paramReplacementMap lookup structures to align capturing groups with handler indices, returning a fully operational RegExp instance anchored with a caret (^) src/router/reg-exp-router/trie.ts:55-73.

buildRegExp(): [RegExp, ReplacementMap, ReplacementMap] {
  let regexp = this.#root.buildRegExpStr()
  if (regexp === '') {
    return [/^$/, [], []] // never match
  }
 
  let captureIndex = 0
  const indexReplacementMap: ReplacementMap = []
  const paramReplacementMap: ReplacementMap = []
 
  regexp = regexp.replace(/#(\d+)|@(\d+)|\.\*\$/g, (_, handlerIndex, paramIndex) => {
    if (handlerIndex !== undefined) {
      indexReplacementMap[++captureIndex] = Number(handlerIndex)
      return '$()'
    }
    if (paramIndex !== undefined) {
      paramReplacementMap[Number(paramIndex)] = ++captureIndex
      return ''
    }
 
    return ''
  })
 
  return [new RegExp(`^${regexp}`), indexReplacementMap, paramReplacementMap]
}

Sources: src/router/reg-exp-router/trie.ts:49-73

Matcher Execution and Param Extraction

Single-Pass Regex Matching and Static Map Lookups

When a request path is evaluated, the match function first resolves the appropriate Matcher<T> tuple for the HTTP method by checking method-specific matchers or falling back to METHOD_NAME_ALL src/router/reg-exp-router/matcher.ts:10-15.

export function match<R extends Router<T>, T>(this: R, method: string, path: string): Result<T> {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const matchers: MatcherMap<T> = (this as any).buildAllMatchers()
 
  const match = ((method, path) => {
    const matcher = (matchers[method] || matchers[METHOD_NAME_ALL]) as Matcher<T>
 
    const staticMatch = matcher[2][path]
    if (staticMatch) {
      return staticMatch
    }

Sources: src/router/reg-exp-router/matcher.ts:10-20

Before executing regular expression evaluation, the router checks the StaticMap (matcher[2]) for an exact path match src/router/reg-exp-router/matcher.ts:17-20. If a static entry exists, it returns the precompiled handler list and empty parameters directly, avoiding regex overhead entirely src/router/reg-exp-router/matcher.ts:17-20. When the path is dynamic, path.match(matcher[0]) executes a single-pass regular expression match across the compiled routing tree src/router/reg-exp-router/matcher.ts:22-25.

  const match = path.match(matcher[0])
  if (!match) {
    return [[], emptyParam]
  }
 
  const index = match.indexOf('', 1)
  return [matcher[1][index], match]
}) as Router<T>['match']

Sources: src/router/reg-exp-router/matcher.ts:21-29

Note

If the regular expression test fails (!match), the matcher immediately returns an empty handler list and an empty parameter array [[], emptyParam] src/router/reg-exp-router/matcher.ts:23-25.

Sources: src/router/reg-exp-router/matcher.ts:23-25

Index-to-Handler Dispatch and Capture-Group Extraction

Once a successful regex match occurs, the router determines which route branch matched by scanning the match array for the handler index marker src/router/reg-exp-router/matcher.ts:27-28. The expression match.indexOf('', 1) finds the empty string boundary or group index corresponding to the active route branch src/router/reg-exp-router/matcher.ts:27.

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

Sources: src/router/reg-exp-router/matcher.ts:27-28

The resulting index maps directly to matcher[1], which holds the handler array and pre-mapped parameter keys for that specific route branch src/router/reg-exp-router/matcher.ts:28. The matched RegExp result array itself is returned alongside the handlers, supplying captured parameter values extracted from dynamic segments src/router/reg-exp-router/matcher.ts:28.

Tip

After the initial match call executes, this.match is dynamically rewritten on the router instance to point directly to the inner closure function, bypassing method lookup overhead on all subsequent requests src/router/reg-exp-router/matcher.ts:14-32.

Sources: src/router/reg-exp-router/matcher.ts:14-32

Ahead-of-Time Prepared Matching

Serialization and Pre-compilation Patterns

To bypass dynamic route compilation overhead during server startup, PreparedRegExpRouter enables ahead-of-time initialization src/router/reg-exp-router/prepared-router.ts:9-17. The helper function buildInitParams provisions a temporary RegExpRouter instance, registers all target paths against METHOD_NAME_ALL, and exports the resulting matcher structures src/router/reg-exp-router/prepared-router.ts:95-154. It constructs a relocateMap that maps individual paths to their corresponding handler indices and parameter index maps src/router/reg-exp-router/prepared-router.ts:111-144.

export const buildInitParams: (params: {
  paths: string[]
}) => ConstructorParameters<typeof PreparedRegExpRouter> = ({ paths }) => {
  const RegExpRouterWithMatcherExport = class<T> extends RegExpRouter<T> {
    buildAndExportAllMatchers() {
      return this.buildAllMatchers()
    }
  }
  const router = new RegExpRouterWithMatcherExport<string>()
  for (const path of paths) {
    router.add(METHOD_NAME_ALL, path, path)
  }
 
  const matchers = router.buildAndExportAllMatchers()
  const all = matchers[METHOD_NAME_ALL] as Matcher<string>
 
  const relocateMap: RelocateMap = {}
  for (const path of paths) {
    if (path === '/*' || path === '*') {
      continue
    }
    all[1].forEach((list, i) => {
      list.forEach(([p, map]) => {
        if (p === path) {
          if (relocateMap[path]) {
            relocateMap[path][0][1] = {
              ...relocateMap[path][0][1],
              ...map,
            }
          } else {
            relocateMap[path] = [[[], map]]
          }
          if (relocateMap[path][0][0].findIndex((j) => j === i) === -1) {
            relocateMap[path][0][0].push(i)
          }
        }
      })
    })
    for (const path2 in all[2]) {
      all[2][path2][0].forEach(([p]) => {
        if (p === path) {
          relocateMap[path] ||= [[[]]]
          const value = path2 === path ? '' : path2
          if (relocateMap[path][0][0].findIndex((v) => v === value) === -1) {
            relocateMap[path][0][0].push(value)
          }
        }
      })
    }
  }
 
  for (let i = 0, len = all[1].length; i < len; i++) {
    all[1][i] = all[1][i] ? [] : (0 as unknown as HandlerData<string>)
  }
  for (const path in all[2]) {
    all[2][path][0] = []
  }
 
  return [matchers, relocateMap]
}

Sources: src/router/reg-exp-router/prepared-router.ts:95-154

The companion utility serializeInitParams translates these compilation artifacts into stringified JSON formats src/router/reg-exp-router/prepared-router.ts:156-165. To preserve native regular expression objects across serialization boundaries, it converts RegExp instances into tagged string placeholders (##...##) and restores them via regex replacement during output generation src/router/reg-exp-router/prepared-router.ts:160-164.

export const serializeInitParams: (
  params: ConstructorParameters<typeof PreparedRegExpRouter>
) => string = ([matchers, relocateMap]) => {
  // Embed the regular expression as a result of `toString()` so that it can be evaluated as JavaScript.
  const matchersStr = JSON.stringify(matchers, (_, value) =>
    value instanceof RegExp ? `##${value.toString()}##` : value
  ).replace(/"##(.+?)##"/g, (_, str) => str.replace(/\\\\/g, '\\'))
  const relocateMapStr = JSON.stringify(relocateMap)
  return `[${matchersStr},${relocateMapStr}]`
}

Sources: src/router/reg-exp-router/prepared-router.ts:156-165

Runtime Mechanics and Relocation Maps

At runtime, PreparedRegExpRouter accepts precompiled MatcherMap and RelocateMap collections directly through its constructor src/router/reg-exp-router/prepared-router.ts:14-17. When additional routes are added dynamically via add, the router references the relocateMap to insert handlers into pre-allocated matcher slots without rebuilding the trie or re-synthesizing regular expression patterns src/router/reg-exp-router/prepared-router.ts:47-86.

add(method: string, path: string, handler: T) {
  if (!this.#matchers[method]) {
    const all = this.#matchers[METHOD_NAME_ALL] as Matcher<T>
    const staticMap = {} as StaticMap<T>
    for (const key in all[2]) {
      staticMap[key] = [all[2][key][0].slice(), emptyParam] as Result<T>
    }
    this.#matchers[method] = [
      all[0],
      all[1].map((list) => (Array.isArray(list) ? list.slice() : 0)) as HandlerData<T>[],
      staticMap,
    ]
  }
 
  if (path === '/*' || path === '*') {
    const handlerData: [T, ParamIndexMap] = [handler, {}]
    if (method === METHOD_NAME_ALL) {
      for (const m in this.#matchers) {
        this.#addWildcard(m, handlerData)
      }
    } else {
      this.#addWildcard(method, handlerData)
    }
    return
  }
 
  const data = this.#relocateMap[path]
  if (!data) {
    throw new Error(`Path ${path} is not registered`)
  }
  for (const [indexes, map] of data) {
    if (method === METHOD_NAME_ALL) {
      for (const m in this.#matchers) {
        this.#addPath(m, path, handler, indexes, map)
      }
    } else {
      this.#addPath(method, path, handler, indexes, map)
    }
  }
}

Sources: src/router/reg-exp-router/prepared-router.ts:47-86

Warning

If a route path is registered via add that was not included in the initial path array passed to buildInitParams, the router throws an error stating that the path is not registered src/router/reg-exp-router/prepared-router.ts:73-76.

Sources: src/router/reg-exp-router/prepared-router.ts:73-76

Parameter / PropertyTypeDescription
#matchersMatcherMap<T>Method-indexed collection containing compiled regular expressions, handler lists, and static maps src/router/reg-exp-router/prepared-router.ts:9-17
#relocateMapRelocateMapMapping of paths to route index arrays and parameter index configurations src/router/reg-exp-router/prepared-router.ts:7-12
buildInitParamsFunctionPre-builds matchers and computes relocation targets from a set of paths src/router/reg-exp-router/prepared-router.ts:95-97
serializeInitParamsFunctionSerializes matchers and relocation maps into an evaluable string representation src/router/reg-exp-router/prepared-router.ts:156-158

Sources: src/router/reg-exp-router/prepared-router.ts:7-17, src/router/reg-exp-router/prepared-router.ts:95-97, src/router/reg-exp-router/prepared-router.ts:156-158

Was this page helpful?