Skip to content

NeaByteLab/Fast-Router

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Fast Router Module type: CJS+ESM npm version JSR Node.js CI

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.