Relevant source files
The following files were used as context for generating this wiki page:
- src/router/linear-router/router.ts
- src/router/pattern-router/router.ts
- src/router/reg-exp-router/router.ts
- src/router/reg-exp-router/prepared-router.ts
- src/helper/route/index.ts
- src/types.ts
- src/router/pattern-router/index.ts
- src/router/reg-exp-router/matcher.ts
- src/utils/url.ts
- src/preset/tiny.ts
- src/hono-base.ts
- src/router/linear-router/index.ts
- src/preset/quick.ts
- src/middleware/ip-restriction/index.ts
- src/router/trie-router/router.ts
- src/router/trie-router/node.ts
- src/request.ts
- src/router/smart-router/router.ts
- package.json
- src/router/reg-exp-router/index.ts
- src/router.ts
- src/router/reg-exp-router/node.ts
- src/router/reg-exp-router/trie.ts
- src/router/trie-router/index.ts
- src/router/smart-router/index.ts
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 Condition | Path Structure Indicators | Parameter Extraction Mechanism |
|---|---|---|
| Static Route | !hasStar && !hasLabel | emptyParams (no parameters) |
| Wildcard Route | hasStar && !hasLabel | emptyParams via substring and slash checks |
| Labeled Parameter Route | hasLabel && !hasStar | Record<string, string> via splitPathRe and regex indices |
| Unsupported Combination | hasLabel && hasStar | Throws 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 Implementation | Error Trigger Location | Failure Condition | Thrown Exception |
|---|---|---|---|
LinearRouter | match() method | Route path contains both label (:) and wildcard (*) | UnsupportedPathError |
PatternRouter | add() method | new RegExp() constructor throws on malformed syntax | UnsupportedPathError |
} 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 Key | Types Definition Path | Import / ESM Path | Require / 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
Related
On This Page
OverviewMinimalist Router Architecture and TradeoffsArchitectural RolesDesign Tradeoffs and LimitationsLinear Route Registration and ExecutionInternal Storage and Route RegistrationSequential Scanning and Matching BranchesPattern Matching and Dynamic EvaluationPath Pattern Compilation and Segment ParsingRequest Match Execution and Parameter ExtractionPath Support and Fallback ErrorsHandling of Unsupported Syntax and Optional ParametersThrowing of UnsupportedPathErrorPreset Integration and Size OptimizationDistribution Entry Points and Preset RoutingType Version MappingsRelated