Skip to main content
doc0blade47/honodocumentation home
Sign in

Linear and Pattern Routers

Last updated Oct 12, 2026100% grounded
Relevant source files

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

Overview

Linear and Pattern routers provide lightweight path-matching backends designed to minimize bundle footprints and execution overhead in constrained environments. Operating as alternative routing engines alongside Trie and Smart Routers and RegExp Router, these implementations satisfy the unified Router Contracts interface while trading complex trie structures and regex compilation for straightforward linear scanning or direct pattern compilation. By storing routes in linear arrays or evaluating dynamic path segments via native regular expressions, they power compact distribution builds such as [[Presets and Builds]] while seamlessly integrating with the core routing lifecycle.

Sources: src/router/linear-router/router.ts:11-23, src/router/pattern-router/router.ts:8-42, src/router.ts:29-52

Minimalist Router Architecture and Tradeoffs

Architectural Roles

LinearRouter and PatternRouter implement the unified Router<T> interface through straightforward, dependency-free mechanisms designed for minimal code size and memory overhead. LinearRouter stores registered routes inside a private array (#routes: [string, string, T][]), processing them sequentially during route matching without compiling regular expressions at definition time. Conversely, PatternRouter compiles path definitions directly into native RegExp instances upon registration, storing them alongside methods in its #routes array.

Sources: src/router/linear-router/router.ts:11-23, src/router/pattern-router/router.ts:8-42

Design Tradeoffs and Limitations

Both routers balance implementation simplicity against runtime performance characteristics. Because LinearRouter evaluates path segments and substring positions via string index checks (indexOf) and custom iteration loops, it avoids regex compilation overhead during initialization but incurs linear scan costs ($O(N)$) proportional to the registered route count during request matching.

PatternRouter delegates matching directly to the JavaScript engine's regex execution engine using named capture groups (match.groups), but both routers enforce strict structural boundaries on path syntax. When confronted with unsupported path combinations—such as combining labels and wildcards within the same route path—both implementations throw an UnsupportedPathError.

Sources: src/router/linear-router/router.ts:25-143, src/router/pattern-router/router.ts:44-59

Warning

Combining wildcard characters (*) and labeled parameters (:name) within a single route path triggers an UnsupportedPathError in both LinearRouter and PatternRouter.

Sources: src/router/linear-router/router.ts:136-138, src/router/pattern-router/router.ts:33-41

Linear Route Registration and Execution

Internal Storage and Route Registration

LinearRouter implements the Router<T> interface by maintaining a private tuple array #routes: [string, string, T][] that stores the HTTP method, route pattern, and handler for every registered endpoint. When add(method, path, handler) is invoked, the router checks the incoming path for optional parameter syntax via checkOptionalParameter(path). If optional parameters expand the path definition into multiple variations, each resulting path is appended as a distinct tuple entry into #routes.

export class LinearRouter<T> implements Router<T> {
  name: string = 'LinearRouter'
  #routes: [string, string, T][] = []
 
  add(method: string, path: string, handler: T) {
    for (
      let i = 0, paths = checkOptionalParameter(path) || [path], len = paths.length;
      i < len;
      i++
    ) {
      this.#routes.push([method, paths[i], handler])
    }
  }

Sources: src/router/linear-router/router.ts:11-23

Sequential Scanning and Matching Branches

During match(method, path), LinearRouter iterates through #routes using a labeled loop (ROUTES_LOOP) to evaluate every registered route sequentially. Each candidate tuple's method is tested for an exact match against the incoming request method or the catch-all constant METHOD_NAME_ALL. If the method matches, the evaluation branches based on the structural characteristics of the routePath string: static paths, trailing or embedded wildcards, labeled parameters, or combined wildcards and labels.

match(method: string, path: string): Result<T> {
  const handlers: [T, Params][] = []
  ROUTES_LOOP: for (let i = 0, len = this.#routes.length; i < len; i++) {
    const [routeMethod, routePath, handler] = this.#routes[i]
    if (routeMethod === method || routeMethod === METHOD_NAME_ALL) {
      if (routePath === '*' || routePath === '/*') {
        handlers.push([handler, emptyParams])
        continue
      }
 
      const hasStar = routePath.indexOf('*') !== -1
      const hasLabel = routePath.indexOf(':') !== -1
      if (!hasStar && !hasLabel) {
        if (routePath === path || routePath + '/' === path) {
          handlers.push([handler, emptyParams])
        }

Sources: src/router/linear-router/router.ts:25-40

Route Type ConditionPath Structure IndicatorsParameter Extraction Mechanism
Static Route!hasStar && !hasLabelemptyParams (no parameters)
Wildcard RoutehasStar && !hasLabelemptyParams via substring and slash checks
Labeled Parameter RoutehasLabel && !hasStarRecord<string, string> via splitPathRe and regex indices
Unsupported CombinationhasLabel && hasStarThrows UnsupportedPathError
const hasStar = routePath.indexOf('*') !== -1
const hasLabel = routePath.indexOf(':') !== -1
if (!hasStar && !hasLabel) {
  if (routePath === path || routePath + '/' === path) {
    handlers.push([handler, emptyParams])
  }
} else if (hasStar && !hasLabel) {
  const endsWithStar = routePath.charCodeAt(routePath.length - 1) === 42
  const parts = (endsWithStar ? routePath.slice(0, -2) : routePath).split(splitByStarRe)
 
  const lastIndex = parts.length - 1
  for (let j = 0, pos = 0, len = parts.length; j < len; j++) {
    const part = parts[j]
    const index = path.indexOf(part, pos)
    if (index !== pos) {
      continue ROUTES_LOOP
    }
    pos += part.length
    if (j === lastIndex) {
      if (
        !endsWithStar &&
        pos !== path.length &&
        !(pos === path.length - 1 && path.charCodeAt(pos) === 47)
      ) {
        continue ROUTES_LOOP
      }
    } else {
      const index = path.indexOf('/', pos)
      if (index === -1) {
        continue ROUTES_LOOP
      }
      pos = index
    }
  }
  handlers.push([handler, emptyParams])
} else if (hasLabel && !hasStar) {
  const params: Record<string, string> = Object.create(null)
  const parts = routePath.match(splitPathRe) as string[]
 
  const lastIndex = parts.length - 1
  for (let j = 0, pos = 0, len = parts.length; j < len; j++) {
    if (pos === -1 || pos >= path.length) {
      continue ROUTES_LOOP
    }
 
    const part = parts[j]
    if (part.charCodeAt(1) === 58) {
      if (path.charCodeAt(pos) !== 47) {
        continue ROUTES_LOOP
      }
 
      // /:label
      let name = part.slice(2)
      let value
 
      if (name.charCodeAt(name.length - 1) === 125) {
        // :label{pattern}
        const openBracePos = name.indexOf('{')
        const next = parts[j + 1]
        const lookahead = next && next[1] !== ':' && next[1] !== '*' ? `(?=${next})` : ''
        const pattern = name.slice(openBracePos + 1, -1) + lookahead
        const restPath = path.slice(pos + 1)
        const match = new RegExp(pattern, 'd').exec(restPath) as RegExpMatchArrayWithIndices
        if (!match || match.indices[0][0] !== 0 || match.indices[0][1] === 0) {
          continue ROUTES_LOOP
        }
        name = name.slice(0, openBracePos)
        value = restPath.slice(...match.indices[0])
        pos += match.indices[0][1] + 1
      } else {
        let endValuePos = path.indexOf('/', pos + 1)
        if (endValuePos === -1) {
          if (pos + 1 === path.length) {
            continue ROUTES_LOOP
          }
          endValuePos = path.length
        }
        value = path.slice(pos + 1, endValuePos)
        pos = endValuePos
      }

Sources: src/router/linear-router/router.ts:35-138

Note

LinearRouter does not halt execution upon finding the first matching route; it continues scanning the entire #routes array and collects all matching handlers into a results array.

Sources: src/router/linear-router/router.ts:25-143

Pattern Matching and Dynamic Evaluation

Path Pattern Compilation and Segment Parsing

PatternRouter registers routes by parsing path strings into compiled regular expressions stored in #routes: Route<T>[], where each tuple contains [RegExp, string, T] representing the compiled pattern, HTTP method, and handler. When add(method, path, handler) runs, it checks whether path ends with an asterisk and strips /* if present. If path ends with a question mark, it strips the ? and recursively invokes this.add on the path with the final segment removed via path.replace(/\/[^/]+$/, '').

add(method: string, path: string, handler: T) {
  const endsWithWildcard = path.at(-1) === '*'
  if (endsWithWildcard) {
    path = path.slice(0, -2)
  }
  if (path.at(-1) === '?') {
    path = path.slice(0, -1)
    this.add(method, path.replace(/\/[^/]+$/, ''), handler)
  }

Sources: src/router/pattern-router/router.ts:12-20

The router splits and transforms the path into regex tokens using path.match(/\/?(:\w+(?:{(?:(?:{[\d,]+})|[^}])+})?)|\/?[^\/\?]+/g). Each matched segment is mapped: labeled parameters matching ^\/:([^{]+)(?:{(.*)})? compile into named capture groups /(?<${match[1]}>${match[2] || '[^/]+'}), wildcard segments (/*) translate to /[^/]+, and static segments escape special characters using /[.\\+*[^\]$()]/g. The resulting parts are joined between ^ and /?$ (or $/ if ending with a wildcard) to instantiate the RegExp object.

 
const parts = (path.match(/\/?(:\w+(?:{(?:(?:{[\d,]+})|[^}])+})?)|\/?[^\/\?]+/g) || []).map(
  (part) => {
    const match = part.match(/^\/:([^{]+)(?:{(.*)})?/)
    return match
      ? `/(?<${match[1]}>${match[2] || '[^/]+'})`
      : part === '/*'
        ? '/[^/]+'
        : part.replace(/[.\\+*[^\]$()]/g, '\\$&')
  }
)
 
try {
  this.#routes.push([
    new RegExp(`^${parts.join('')}${endsWithWildcard ? '' : '/?$'}`),
    method,
    handler,
  ])

Sources: src/router/pattern-router/router.ts:21-38

Tip

Custom regex constraints inside route parameters—such as numeric bounds or character classes defined in curly braces—are preserved directly during segment extraction and compiled into the named capture group expression.

Sources: src/router/pattern-router/router.ts:24-26

Request Match Execution and Parameter Extraction

During match(method, path), PatternRouter initializes an empty handlers: [T, Params][] array and loops through every entry in #routes. It tests each route method against the incoming method or METHOD_NAME_ALL. When the method matches, it executes the compiled regex pattern against the request path. If pattern.exec(path) succeeds, it pushes the handler and extracted match.groups (falling back to emptyParams created via Object.create(null)) into the handlers array, returning [handlers] upon completion.

match(method: string, path: string): Result<T> {
  const handlers: [T, Params][] = []
 
  for (let i = 0, len = this.#routes.length; i < len; i++) {
    const [pattern, routeMethod, handler] = this.#routes[i]
 
    if (routeMethod === method || routeMethod === METHOD_NAME_ALL) {
      const match = pattern.exec(path)
      if (match) {
        handlers.push([handler, match.groups || emptyParams])
      }
    }
  }
 
  return [handlers]
}

Sources: src/router/pattern-router/router.ts:44-59

Path Support and Fallback Errors

Handling of Unsupported Syntax and Optional Parameters

Both LinearRouter and PatternRouter enforce specific path composition rules and handle optional trailing parameters during route registration. In LinearRouter, the add method uses checkOptionalParameter(path) from shared URL utilities to expand optional path variations before pushing them into the #routes array.

add(method: string, path: string, handler: T) {
  for (
    let i = 0, paths = checkOptionalParameter(path) || [path], len = paths.length;
    i < len;
    i++
  ) {
    this.#routes.push([method, paths[i], handler])
  }
}

Sources: src/router/linear-router/router.ts:15-23

In PatternRouter, optional parameters ending with a question mark (?) are processed during registration by stripping the trailing ? character and recursively invoking this.add with the preceding path segment removed via path.replace(/\/[^/]+$/, '').

if (path.at(-1) === '?') {
  path = path.slice(0, -1)
  this.add(method, path.replace(/\/[^/]+$/, ''), handler)
}

Sources: src/router/pattern-router/router.ts:17-20

Warning

Combining labeled parameters and wildcards within the same route path in LinearRouter (hasLabel && hasStar) is unsupported and explicitly triggers an error.

Sources: src/router/linear-router/router.ts:136-138

Throwing of UnsupportedPathError

When path compilation or syntax evaluation encounters malformed expressions or unsupported combinations, both routers throw an UnsupportedPathError imported from the core router module. In LinearRouter, this occurs synchronously inside match when both label and star flags evaluate to true on a route path.

} else if (hasLabel && hasStar) {
  throw new UnsupportedPathError()
}

Sources: src/router/linear-router/router.ts:136-138

In PatternRouter, route compilation is wrapped in a try...catch block. If instantiating the dynamic RegExp object fails due to invalid regular expression syntax or unsupported token combinations in path segments, the catch block intercepts the error and throws an UnsupportedPathError.

try {
  this.#routes.push([
    new RegExp(`^${parts.join('')}${endsWithWildcard ? '' : '/?$'}`),
    method,
    handler,
  ])
} catch {
  throw new UnsupportedPathError()
}

Sources: src/router/pattern-router/router.ts:33-41

Router ImplementationError Trigger LocationFailure ConditionThrown Exception
LinearRoutermatch() methodRoute path contains both label (:) and wildcard (*)UnsupportedPathError
PatternRouteradd() methodnew RegExp() constructor throws on malformed syntaxUnsupportedPathError
} else if (hasLabel && hasStar) {
  throw new UnsupportedPathError()
}
…
try {
  this.#routes.push([
    new RegExp(`^${parts.join('')}${endsWithWildcard ? '' : '/?$'}`),
    method,
    handler,
  ])
} catch {
  throw new UnsupportedPathError()
}

Sources: src/router/linear-router/router.ts:136-138, src/router/pattern-router/router.ts:33-41

Preset Integration and Size Optimization

Distribution Entry Points and Preset Routing

Hono configures package export maps in package.json to expose specialized distribution bundles and entry points, including lightweight presets like tiny and quick, alongside individual router modules such as [[LinearRouter]] and [[PatternRouter]].

"./router/linear-router": {
  "types": "./dist/types/router/linear-router/index.d.ts",
  "import": "./dist/router/linear-router/index.js",
  "require": "./dist/cjs/router/linear-router/index.js"
},

Sources: package.json:308-312

Export Target KeyTypes Definition PathImport / ESM PathRequire / CJS Path
./router/linear-router./dist/types/router/linear-router/index.d.ts./dist/router/linear-router/index.js./dist/cjs/router/linear-router/index.js
./router/pattern-router./dist/types/router/pattern-router/index.d.ts./dist/router/pattern-router/index.js./dist/cjs/router/pattern-router/index.js
./router/trie-router./dist/types/router/trie-router/index.d.ts./dist/router/trie-router/index.js./dist/cjs/router/trie-router/index.js
./router/reg-exp-router./dist/types/router/reg-exp-router/index.d.ts./dist/router/reg-exp-router/index.js./dist/cjs/router/reg-exp-router/index.js
./router/smart-router./dist/types/router/smart-router/index.d.ts./dist/router/smart-router/index.js./dist/cjs/router/smart-router/index.js
"./router/reg-exp-router": {
  "types": "./dist/types/router/reg-exp-router/index.d.ts",
  "import": "./dist/router/reg-exp-router/index.js",
  "require": "./dist/cjs/router/reg-exp-router/index.js"
},
"./router/smart-router": {
  "types": "./dist/types/router/smart-router/index.d.ts",
  "import": "./dist/router/smart-router/index.js",
  "require": "./dist/cjs/router/smart-router/index.js"
},
"./router/trie-router": {
  "types": "./dist/types/router/trie-router/index.d.ts",
  "import": "./dist/router/trie-router/index.js",
  "require": "./dist/cjs/router/trie-router/index.js"
},
"./router/pattern-router": {
  "types": "./dist/types/router/pattern-router/index.d.ts",
  "import": "./dist/router/pattern-router/index.js",
  "require": "./dist/cjs/router/pattern-router/index.js"
},
"./router/linear-router": {
  "types": "./dist/types/router/linear-router/index.d.ts",
  "import": "./dist/router/linear-router/index.js",
  "require": "./dist/cjs/router/linear-router/index.js"
},

Sources: package.json:288-312

Type Version Mappings

Type definitions for lightweight bundle targets and router exports are mapped explicitly within the typesVersions configuration block of package.json, ensuring correct TypeScript resolution in size-constrained environments Presets and Builds.

"tiny": [
  "./dist/types/preset/tiny"
],
"quick": [
  "./dist/types/preset/quick"
],

Sources: package.json:425-430

Was this page helpful?