Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

NeaByteLab/Fast-Router

Repository files navigation

A fast, versatile router with radix tree structure for JavaScript

Note

This project is inspired by rou3 and uses similar routing algorithm and tree structure. It provides a class-based API as an alternative to rou3's functional approach, suitable for object-oriented use cases.

Table of Contents

Quick Start

Installation

# npm package
npm install @neabyte/fast-router
# Deno module
deno add jsr:@neabyte/fast-router

Basic Usage

import { FastRouter } from '@neabyte/fast-router'
// Create a new router instance
const router = new FastRouter()
// Add routes with data
router.add('GET', '/users/:id', { handler: 'getUser' })
router.add('GET', '/users', { handler: 'listUsers' })
router.add('GET', '/posts/*', { handler: 'posts' })
// Find matching route
const match = router.find('GET', '/users/123')
console.log(match?.data?.handler) // 'getUser'
console.log(match?.params?.id) // '123'

Examples

Data Type Flexibility

Fast Router supports any data type via TypeScript generics. You're not limited to handler objects!

// String data
const router1 = new FastRouter<string>()
router1.add('GET', '/users/:id', 'getUser')
const match1 = router1.find('GET', '/users/123')
console.log(match1?.data) // 'getUser'
// Number data
const router2 = new FastRouter<number>()
router2.add('GET', '/config/:key', 42)
const match2 = router2.find('GET', '/config/value')
console.log(match2?.data) // 42
// Array data
const router3 = new FastRouter<string[]>()
router3.add('GET', '/posts', ['list', 'posts'])
const match3 = router3.find('GET', '/posts')
console.log(match3?.data) // ['list', 'posts']
// Function data
const handler = (req: Request) => new Response('ok')
const router4 = new FastRouter<typeof handler>()
router4.add('GET', '/api/data', handler)
const match4 = router4.find('GET', '/api/data')
console.log(typeof match4?.data) // 'function'
// Complex object
interface RouteConfig {
 handler: string
 middleware: string[]
 timeout: number
}
const router5 = new FastRouter<RouteConfig>()
router5.add('GET', '/secure', {
 handler: 'protected',
 middleware: ['auth', 'rate-limit'],
 timeout: 5000
})
const match5 = router5.find('GET', '/secure')
console.log(match5?.data) // Full object
// Class instance
class MyHandler {
 name: string
 constructor(name: string) {
 this.name = name
 }
}
const router6 = new FastRouter<MyHandler>()
router6.add('GET', '/custom', new MyHandler('test'))
const match6 = router6.find('GET', '/custom')
console.log(match6?.data?.name) // 'test'
// No data (undefined)
const router7 = new FastRouter<undefined>()
router7.add('GET', '/ping')
const match7 = router7.find('GET', '/ping')
console.log(match7?.data) // null

Named Parameters

const router = new FastRouter()
// Route with named parameter
router.add('GET', '/users/:id', { handler: 'getUser' })
const userMatch = router.find('GET', '/users/123')
console.log(userMatch?.data?.handler) // 'getUser'
console.log(userMatch?.params?.id) // '123'
// Route with named parameters
router.add('GET', '/users/:id/posts/:postId', { handler: 'getPost' })
const postMatch = router.find('GET', '/users/123/posts/456')
console.log(postMatch?.data?.handler) // 'getPost'
console.log(postMatch?.params?.id) // '123'
console.log(postMatch?.params?.postId) // '456'

Wildcard Parameters

const router = new FastRouter()
// Single wildcard (matches one segment)
router.add('GET', '/posts/*', { handler: 'posts' })
const match = router.find('GET', '/posts/javascript')
console.log(match?.data?.handler) // 'posts'
console.log(match?.params?._0) // 'javascript'

Catch-All Wildcard

const router = new FastRouter()
// Catch-all wildcard (matches multiple segments)
router.add('GET', '/files/**', { handler: 'files' })
const match = router.find('GET', '/files/docs/user/guide.pdf')
console.log(match?.data?.handler) // 'files'
// Named catch-all
router.add('GET', '/files/**:name', { handler: 'fileName' })
const namedMatch = router.find('GET', '/files/docs/user/guide.pdf')
console.log(namedMatch?.params?.name) // 'docs/user/guide.pdf'

Regex Parameter Validation

const router = new FastRouter()
// Validate numeric ID
router.add('GET', '/user/:id(\\d+)', { handler: 'numericUser' })
const validMatch = router.find('GET', '/user/123')
console.log(validMatch?.data?.handler) // 'numericUser'
const invalidMatch = router.find('GET', '/user/abc')
console.log(invalidMatch) // undefined (regex validation failed)
// Validate phone format
router.add('GET', '/phone/:number(\\d{3}-\\d{3}-\\d{4})', { handler: 'phoneFormat' })
const phoneMatch = router.find('GET', '/phone/123-456-7890')
console.log(phoneMatch?.params?.number) // '123-456-7890'

HTTP Method Support

const router = new FastRouter()
// Method-specific route (GET)
router.add('GET', '/users/:id', { handler: 'getUser' })
const getMatch = router.find('GET', '/users/123')
console.log(getMatch?.data?.handler) // 'getUser'
// Method-specific route (PUT)
router.add('PUT', '/users/:id', { handler: 'updateUser' })
const putMatch = router.find('PUT', '/users/123')
console.log(putMatch?.data?.handler) // 'updateUser'
// Method-agnostic route
router.add('', '/api/data', { handler: 'anyMethod' })
const anyMatch = router.find('POST', '/api/data')
console.log(anyMatch?.data?.handler) // 'anyMethod'

Static vs Parameter Routes

const router = new FastRouter()
// Static route
router.add('GET', '/users/admin', { handler: 'adminRoute' })
const adminMatch = router.find('GET', '/users/admin')
console.log(adminMatch?.data?.handler) // 'adminRoute'
// Parameter route
router.add('GET', '/users/:id', { handler: 'userRoute' })
const userMatch = router.find('GET', '/users/123')
console.log(userMatch?.data?.handler) // 'userRoute'

Trailing Slash Normalization

const router = new FastRouter()
// Add route with trailing slash
router.add('GET', '/api/data', { handler: 'data' })
// Both paths work
const withSlash = router.find('GET', '/api/data/')
const withoutSlash = router.find('GET', '/api/data')
console.log(withSlash?.data?.handler) // 'data'
console.log(withoutSlash?.data?.handler) // 'data'

Complex Nested Routes

const router = new FastRouter()
// Multiple parameters and wildcards
router.add('GET', '/api/:version/posts/:id/comments', { handler: 'comments' })
const match = router.find('GET', '/api/v1/posts/123/comments')
console.log(match?.params?.version) // 'v1'
console.log(match?.params?.id) // '123'
// Mixed wildcards
router.add('GET', '/data/:id/*/**:rest', { handler: 'mixed' })
const wildMatch = router.find('GET', '/data/123/x/y/z')
console.log(wildMatch?.params?.id) // '123'
console.log(wildMatch?.params?._0) // 'x'
console.log(wildMatch?.params?.rest) // 'y/z'

Disable Parameter Extraction

const router = new FastRouter()
// Add route with parameter
router.add('GET', '/users/:id', { handler: 'getUser' })
// Extract parameters (default)
const withParams = router.find('GET', '/users/123')
console.log(withParams?.params?.id) // '123'
// Disable parameter extraction
const withoutParams = router.find('GET', '/users/123', { params: false })
console.log(withoutParams?.params) // undefined

Route Removal

const router = new FastRouter()
// Add routes with different methods
router.add('GET', '/users/:id', { handler: 'getUser' })
router.add('PUT', '/users/:id', { handler: 'updateUser' })
router.add('GET', '/posts/*', { handler: 'posts' })
// Remove specific method route
const removed = router.remove('GET', '/users/:id')
console.log(removed) // true
// Verify removal
const getMatch = router.find('GET', '/users/123')
console.log(getMatch) // undefined (route was removed)
// Other method still works
const putMatch = router.find('PUT', '/users/123')
console.log(putMatch?.data?.handler) // 'updateUser'
// Remove wildcard route
router.remove('GET', '/posts/*')
const postMatch = router.find('GET', '/posts/javascript')
console.log(postMatch) // undefined
// Remove non-existent route returns false
const notFound = router.remove('GET', '/nonexistent')
console.log(notFound) // false

API Reference

add

router.add(method, path, data?)
  • method <string>: HTTP method (GET, POST, PUT, DELETE, etc.). Use empty string.
  • path <string>: The route path pattern (supports :param, *, ** wildcards).
  • data <T>: (Optional) Data to associate with the route.
  • Returns: void
  • Description: Add a route to the router with optional data and HTTP method.

Route Patterns:

  • :param - Named parameter (matches a single segment)
  • :param(regex) - Named parameter with regex validation
  • * - Wildcard (matches a single segment)
  • ** - Catch-all wildcard (matches remaining segments)
  • **:name - Named catch-all wildcard

find

router.find(method, path, opts?)
  • method <string>: HTTP method to match. Use empty string.
  • path <string>: The path to search for.
  • opts <object>: (Optional) Search options.
    • params <boolean>: (Optional) Whether to extract parameters. Defaults to true.
  • Returns: <RouterMatchedRoute<T> | undefined>
  • Description: Find matching route for the given path and HTTP method. Returns undefined if no match is found.

Return Type:

{
 data: T, // Data associated with the matched route
 params: Record<string, string> | undefined // Extracted route parameters
}

remove

router.remove(method, path)
  • method <string>: HTTP method to remove. Use empty string.
  • path <string>: The route path pattern to remove (supports :param, *, ** wildcards).
  • Returns: <boolean>
  • Description: Remove a route from the router. Returns true if successfully removed, false if not found.

Testing

Run the test suite:

deno task test

Format and lint:

deno task check

Run the benchmarks:

deno task bench

License

This project is licensed under the MIT license. See the LICENSE file for more info.

AltStyle によって変換されたページ (->オリジナル) /