Skip to main content

Overview

The PatternRouter is a straightforward router implementation that converts each route into a regular expression pattern. It’s simple, predictable, and supports most common routing patterns without the complexity of trie structures.

How It Works

The PatternRouter uses a direct approach to routing:
  1. Route Registration: Each route is converted to a RegExp with named capture groups
  2. Pattern Generation: Path segments are transformed into regex patterns
  3. Linear Matching: Routes are tested sequentially until a match is found
  4. Parameter Extraction: Named groups are extracted from the RegExp match

Algorithm Details

  • Each route stored as a [RegExp, method, handler] tuple
  • Dynamic parameters (:name) become named capture groups: (?<name>pattern)
  • Optional parameters (?) create alternate routes
  • Wildcards (*) are supported in routes
  • Routes matched in registration order (first match wins for same score)
  • No preprocessing or optimization

Performance Characteristics

Static Routes

O(n) - Linear search through all routes

Dynamic Routes

O(n) - Tests each route’s RegExp sequentially

Build Time

O(1) - Each route compiled to RegExp immediately

Memory

Low - One RegExp per route

When to Use

  • Small applications with < 50 routes
  • Simple routing requirements
  • Applications where route order matters
  • Prototypes and proof-of-concepts
  • Learning and understanding routing mechanics
  • When predictable, simple behavior is preferred over performance

Configuration

To use the PatternRouter, specify it when creating your Hono instance:
import { Hono } from 'hono'
import { PatternRouter } from 'hono/router/pattern-router'

const app = new Hono({ router: new PatternRouter() })

Usage Examples

Basic Routing

import { Hono } from 'hono'
import { PatternRouter } from 'hono/router/pattern-router'

const app = new Hono({ router: new PatternRouter() })

// Static routes
app.get('/', (c) => c.text('Home'))
app.get('/about', (c) => c.text('About'))
app.get('/contact', (c) => c.text('Contact'))

// Dynamic routes with parameters
app.get('/users/:id', (c) => {
  const id = c.req.param('id')
  return c.json({ userId: id })
})

// Multiple parameters
app.get('/posts/:postId/comments/:commentId', (c) => {
  const { postId, commentId } = c.req.param()
  return c.json({ postId, commentId })
})

Parameter Patterns

// Parameter with RegExp constraint
app.get('/users/:id{[0-9]+}', (c) => {
  const id = c.req.param('id')  // Guaranteed to be numeric
  return c.json({ userId: parseInt(id) })
})

// Complex pattern matching
app.get('/files/:filename{.+\\.(jpg|png|gif)}', (c) => {
  const filename = c.req.param('filename')
  return c.json({ file: filename })
})

// Pattern with multiple constraints
app.get('/api/:version{v[1-9]+}/:resource', (c) => {
  const { version, resource } = c.req.param()
  return c.json({ version, resource })
})

Optional Parameters

// Optional trailing parameter
app.get('/docs/:page?', (c) => {
  const page = c.req.param('page') || 'index'
  return c.json({ page })
})

// Optional parameter in path
app.get('/api/v1/users/:id?', (c) => {
  const id = c.req.param('id')
  if (id) {
    return c.json({ user: id })
  }
  return c.json({ users: 'all' })
})

// Multiple optional segments
app.get('/blog/:year?/:month?/:day?', (c) => {
  const { year, month, day } = c.req.param()
  return c.json({ year, month, day })
})

Wildcard Routes

// Trailing wildcard
app.get('/static/*', (c) => {
  return c.text('Static file handler')
})

// Wildcard matches rest of path
app.get('/files/public/*', (c) => {
  return c.text('Public files')
})

// Without trailing slash
app.get('/api/v1/*', (c) => {
  return c.json({ api: 'v1', path: c.req.path })
})

Route Order Matters

// ✅ Correct order: Specific routes first
app.get('/users/me', (c) => c.json({ user: 'current' }))
app.get('/users/:id', (c) => c.json({ user: c.req.param('id') }))
// GET /users/me → matches first route ✓

// ❌ Wrong order: Generic route shadows specific
app.get('/users/:id', (c) => c.json({ user: c.req.param('id') }))
app.get('/users/me', (c) => c.json({ user: 'current' }))
// GET /users/me → matches first route as :id = "me" ✗

How Pattern Matching Works

Pattern Conversion

// Route: '/users/:id/posts/:postId'
// Becomes RegExp: /^\/users\/(?<id>[^/]+)\/posts\/(?<postId>[^/]+)\/?$/

// Route: '/files/:name{[a-z]+.txt}'
// Becomes RegExp: /^\/files\/(?<name>[a-z]+\.txt)\/?$/

// Route: '/api/*'
// Becomes RegExp: /^\/api\//

Matching Process

// Conceptual matching algorithm
function match(method: string, path: string) {
  const handlers = []
  
  for (const [pattern, routeMethod, handler] of routes) {
    if (routeMethod === method || routeMethod === 'ALL') {
      const match = pattern.exec(path)
      if (match) {
        handlers.push([handler, match.groups || {}])
      }
    }
  }
  
  return [handlers]
}

Example Flow

app.get('/api/users', handler1)
app.get('/api/:resource', handler2)
app.get('/api/*', handler3)

// Request: GET /api/users
// 1. Test /^\/api\/users\/?$/  → ✓ match → add handler1
// 2. Test /^\/api\/(?<resource>[^/]+)\/?$/ → ✓ match → add handler2
// 3. Test /^\/api\// → ✓ match → add handler3
// Returns: [handler1, handler2, handler3] with appropriate params

Advanced Features

Custom Pattern Syntax

PatternRouter supports custom RegExp patterns within parameters:
// Numeric IDs only
app.get('/items/:id{[0-9]+}', handler)

// Slugs (lowercase, hyphens)
app.get('/posts/:slug{[a-z0-9-]+}', handler)

// Date format (YYYY-MM-DD)
app.get('/events/:date{\\d{4}-\\d{2}-\\d{2}}', handler)

// UUID format
app.get('/resources/:uuid{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}', handler)

Nested Parameters

// Deep nesting is supported
app.get('/:org/projects/:project/issues/:issue', (c) => {
  const { org, project, issue } = c.req.param()
  return c.json({ org, project, issue })
})

// Mix of static and dynamic
app.get('/:tenant/api/v1/resources/:id', (c) => {
  const { tenant, id } = c.req.param()
  return c.json({ tenant, resourceId: id })
})

Trailing Slashes

PatternRouter automatically handles trailing slashes:
app.get('/users/:id', handler)

// Both match:
// GET /users/123   ✓
// GET /users/123/  ✓

Limitations

PatternRouter has some limitations:
  • Linear performance: O(n) for all route types
  • No optimization: Static routes not faster than dynamic
  • Order dependent: Route registration order matters
  • No route conflict detection: Overlapping patterns silently return multiple handlers

Unsupported Patterns

// ❌ These may throw UnsupportedPathError or behave unexpectedly:

// Invalid RegExp patterns
app.get('/users/:id{[invalid}', handler)  // Throws during route registration

// Extremely complex patterns may fail
app.get('/complex/:param{(?:..very long pattern..)}', handler)

Performance Optimization Tips

Optimize PatternRouter performance:
  1. Put common routes first - They’ll match faster
  2. Minimize total routes - Each route is tested sequentially
  3. Use specific patterns - Reduce false matches
  4. Group by method - Method check happens before pattern matching

Optimization Example

// ✅ Optimized: Most common routes first
app.get('/', homeHandler)              // Most traffic
app.get('/about', aboutHandler)        // Second most
app.get('/contact', contactHandler)    // Third
app.get('/users/:id', userHandler)     // Less common
app.get('/*', notFoundHandler)         // Catch-all last

// ❌ Not optimized: Catch-all first
app.get('/*', catchAll)                // Matches everything!
app.get('/', homeHandler)              // Never reached

Comparison with Other Routers

FeaturePatternRouterRegExpRouterTrieRouterLinearRouter
Static routesO(n)O(1)O(n)O(n)
Dynamic routesO(n)O(1)O(n×m)O(n)
Pattern supportGoodLimitedExcellentExcellent
Memory usageLowHighMediumLow
SimplicityHighLowMediumHigh
Build timeO(1)O(n log n)O(1)O(1)

Internal Architecture

Route Storage

class PatternRouter<T> {
  name = 'PatternRouter'
  #routes: [RegExp, string, T][] = []  // [pattern, method, handler]
}

Add Method

// Simplified add logic
add(method: string, path: string, handler: T) {
  // Handle wildcards
  const endsWithWildcard = path.endsWith('*')
  
  // Handle optional parameters
  if (path.endsWith('?')) {
    // Creates additional route without optional part
  }
  
  // Convert path to RegExp pattern
  const parts = path.match(/\/?(:\w+(?:{[^}]+})?)|\/?[^\/\?]+/g)
  const pattern = parts.map(convertToRegex).join('')
  
  // Store compiled pattern
  this.#routes.push([new RegExp(pattern), method, handler])
}

Debugging Routes

const router = new PatternRouter()
const app = new Hono({ router })

app.get('/users/:id', handler)

// Access internal routes (for debugging only!)
console.log(router.routes)  // Array of [RegExp, method, handler]

Source Code Reference

The PatternRouter implementation can be found at:
  • Router: src/router/pattern-router/router.ts

See Also

LinearRouter

Simple router without RegExp compilation

RegExpRouter

High-performance compiled router

Routing Guide

Learn about choosing the right router

Route Parameters

Working with dynamic parameters

Build docs developers (and LLMs) love