radix-way
v1.5.1
Published
<h1 align="center">⚡ Radix Way</h1>
Maintainers
Readme
Features
- ⚡ High Performance - Up to 16x faster than find-my-way with optimized radix tree algorithm
- 🪶 Zero Dependencies - Minimal footprint, no external dependencies
- 🎯 Flexible Routing - Supports static, dynamic, wildcard, and regex routes
- 🎨 Advanced Patterns - Multi-parameters, optional params, regex constraints
- 🔗 Middleware Support - Multiple handlers per route for middleware chains
- 🔧 TypeScript Support - Full type safety with generics
- 📖 Well Documented - Comprehensive examples and guides
Table of Contents
- Installation
- Quick Start
- Route Types
- API
- Usage with HTTP Server
- Parameter Mapping
- Performance
- How It Works
- Advanced Features
- TypeScript
- Error Handling
- Debugging
- What's Different from find-my-way?
- Contributing
- License
- Credits
Installation
npm install radix-way
# or
bun add radix-wayQuick Start
import {RadixTree} from 'radix-way';
const router = new RadixTree();
// Add routes
router.insert('GET', '/users', () => 'List users');
router.insert('GET', '/users/:id', () => 'Get user');
router.insert('GET', '/files/*', () => 'Serve files');
// Match routes
const result = router.match('GET', '/users/123');
if (result) {
const [handlers, paramMap, params] = result;
console.log(handlers); // [() => 'Get user']
console.log(paramMap); // {id: 0}
console.log(params); // ['123']
}Route Types
Static Routes
router.insert('GET', '/about', handler);
router.insert('POST', '/login', handler);Dynamic Routes (Parameters)
router.insert('GET', '/users/:id', handler);
router.insert('GET', '/posts/:category/:slug', handler);
const result = router.match('GET', '/users/42');
// result = [[handler], {id: 0}, ['42']]Wildcard Routes
router.insert('GET', '/static/*', handler);
const result = router.match('GET', '/static/css/style.css');
// result = [[handler], {'*': 0}, ['css/style.css']]Regex Routes
// Match only numeric IDs
router.insert('GET', '/users/:id{\\d+}', handler);
// Match slug pattern
router.insert('GET', '/posts/:slug{[a-z0-9-]+}', handler);
// Match with static suffix
router.insert('GET', '/files/:name.jpg', handler);Optional Parameters
router.insert('GET', '/posts/:id?', handler);
// Matches both /posts and /posts/123Regex Pattern Syntax
Use {regex} syntax to add validation constraints to parameters:
// Basic patterns
router.insert('GET', '/users/:id{\\d+}', handler); // Digits only
router.insert('GET', '/posts/:slug{[a-z-]+}', handler); // Lowercase + hyphens
// Character classes
router.insert('GET', '/hex/:color{[0-9a-fA-F]+}', handler); // Hexadecimal
// Quantifiers
router.insert('GET', '/code/:id{[A-Z]{3}\\d{4}}', handler); // ABC1234 format
router.insert('GET', '/slug/:name{[a-z]{3,10}}', handler); // 3-10 chars
// Alternation (OR)
router.insert('GET', '/media/:type{image|video|audio}', handler);
router.insert('GET', '/file.:ext{jpg|png|gif}', handler);
// Groups (non-capturing)
router.insert(
'GET',
'/date/:d{\\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\\d|3[01])}',
handler,
);
// Real-world patterns
router.insert('GET', '/user/:email{[\\w.+-]+@[\\w.-]+\\.\\w+}', handler);
router.insert('GET', '/post/:slug{[a-z0-9]+(?:-[a-z0-9]+)*}', handler);
// Multiple regex params
router.insert('GET', '/api/v:version{\\d+}/users/:id{\\d+}', handler);Important Notes:
- Use
\\for escaping (JavaScript string escaping required) - Regex is applied to the parameter value only, not the full path
- Use non-capturing groups
(?:...)for grouping without capture - The router automatically wraps your pattern in a capturing group
API
new RadixTree<T>()
Create a new router instance.
const router = new RadixTree<Handler>();insert(method: string, path: string, handler: T): void
Add a route to the router. Multiple handlers can be added to the same route for middleware chains.
Parameters:
method- HTTP method (GET, POST, PUT, DELETE, etc.) or 'ALL' for all methodspath- Route pathhandler- Handler function or any value
// Single handler
router.insert('GET', '/api/users/:id', async (req, res) => {
// handler logic
});
// Multiple handlers (middleware pattern)
router.insert('GET', '/api/users', authMiddleware);
router.insert('GET', '/api/users', loggingMiddleware);
router.insert('GET', '/api/users', usersHandler);
// Match all HTTP methods
router.insert('ALL', '/api/health', healthCheckHandler);match(method: string, path: string): [T[], ParamIndexMap, string[]] | null
Match a route and return handlers with parameters.
Returns:
[handlers, paramMap, params]- If match found:handlers- Array of handler functions/values (supports middleware chains)paramMap- Parameter name to index mapping (e.g.,{id: 0, slug: 1})params- Array of extracted parameter values
null- If no match found
const result = router.match('GET', '/api/users/123');
if (result) {
const [handlers, paramMap, params] = result;
const userId = params[paramMap.id]; // '123'
// Execute single handler
if (handlers.length === 1) {
await handlers[0](req, res);
}
// Execute middleware chain
for (const handler of handlers) {
await handler(req, res);
}
}printTree(print?: boolean): void | string
Print the router tree structure for debugging.
Parameters:
print- Whentrue(default), logs to console and returnsvoid. Whenfalse, returns the string representation.
// Print to console (default)
router.printTree();
router.printTree(true);
// Get as string
const treeStr = router.printTree(false);
console.log(treeStr);routeToRegExp(pattern: string): [RegExp, ParamIndexMap]
Utility function to convert route patterns to regular expressions with parameter mapping.
Parameters:
pattern- Route pattern string (e.g.,/users/:id{\\d+})
Returns:
[RegExp, ParamIndexMap]- Tuple containing compiled regex and parameter index mapping
import {routeToRegExp} from 'radix-way';
// Simple parameter
const [regex1, params1] = routeToRegExp('/users/:id');
console.log(regex1); // /^\/users\/([^/]+)\/?$/
console.log(params1); // {id: 0}
// With regex constraint
const [regex2, params2] = routeToRegExp('/users/:id{\\d+}');
console.log(regex2); // /^\/users\/(\d+)\/?$/
console.log(params2); // {id: 0}
// Multiple parameters
const [regex3, params3] = routeToRegExp('/posts/:category/:slug');
console.log(regex3); // /^\/posts\/([^/]+)\/([^/]+)\/?$/
console.log(params3); // {category: 0, slug: 1}
// Wildcard
const [regex4, params4] = routeToRegExp('/static/*');
console.log(regex4); // /^\/static\/(.*)\/?$/
console.log(params4); // {'*': 0}
// Test the regex
const match = regex1.exec('/users/123');
if (match) {
const userId = match[params1.id + 1]; // '123'
console.log('User ID:', userId);
}Usage with HTTP Server
Node.js
import {createServer} from 'http';
import {RadixTree} from 'radix-way';
const router = new RadixTree<(req: any, res: any) => void>();
router.insert('GET', '/', (req, res) => {
res.end('Home');
});
router.insert('GET', '/users/:id', (req, res) => {
res.end('User handler');
});
createServer((req, res) => {
const result = router.match(req.method!, req.url!);
if (!result) {
res.statusCode = 404;
res.end('Not Found');
return;
}
const [handlers, paramMap, params] = result;
// Access params by name
if (paramMap.id !== undefined) {
console.log('User ID:', params[paramMap.id]);
}
// Execute handler(s)
handlers[0](req, res);
}).listen(3000);Bun
import {RadixTree} from 'radix-way';
const router = new RadixTree<(req: Request) => Response>();
router.insert('GET', '/', () => new Response('Home'));
router.insert('GET', '/users/:id', () => new Response('User'));
Bun.serve({
port: 3000,
fetch(req) {
const url = new URL(req.url);
const result = router.match(req.method, url.pathname);
if (!result) {
return new Response('Not Found', {status: 404});
}
const [handlers] = result;
return handlers[0](req);
},
});Parameter Mapping
When routes have named parameters, you can access them by name:
router.insert('GET', '/posts/:category/:slug', handler);
const result = router.match('GET', '/posts/tech/hello-world');
if (result) {
const [handlers, paramMap, params] = result;
const category = params[paramMap.category]; // 'tech'
const slug = params[paramMap.slug]; // 'hello-world'
}Performance
Benchmarked against find-my-way (Node.js v24+):
Test Environment:
- CPU: Apple M4
- RAM: 16 GB
- OS: macOS 26.1 (Build 25B78)
- Node.js: v24.11.1
- npm: 11.6.2
Speed Comparison
| Route Type | radix-way | find-my-way | Performance | | ---------------------- | ------------- | ----------- | ---------------- | | Short Static | 178.96M ops/s | 52.6M ops/s | +240% faster 🚀 | | Static with Same Radix | 216.71M ops/s | 16.5M ops/s | +1213% faster 🔥 | | Dynamic Route | 15.94M ops/s | 9.2M ops/s | +73% faster ⚡ | | Mixed Static Dynamic | 14.56M ops/s | 10.8M ops/s | +35% faster ✅ | | Long Static | 182.33M ops/s | 10.7M ops/s | +1603% faster 💪 | | Wildcard | 25.26M ops/s | 14.0M ops/s | +80% faster ⚡ | | All Together | 5.07M ops/s | 2.1M ops/s | +141% faster 🎯 |
Key Highlights:
- Static routes are exceptionally fast due to optimized radix tree structure with O(1) Map lookup
- Pre-compiled regex for dynamic path validation provides significant performance boost
- Consistently faster than find-my-way in all scenarios, from +35% to +1603% improvement
Run Benchmarks
# Run benchmarks with Bun
bun run bench:bun
# Run benchmarks with Node.js
bun run bench:nodeHow It Works
The router uses a radix tree (compressed trie) data structure:
- Static segments are stored in tree nodes with
Object.create(null)for zero overhead - Prefix matching uses dynamically generated code (
new Function()) for optimal performance - Dynamic parameters (
:param) are handled with parametric nodes - Wildcards (
*) match remaining path segments - Backtracking allows multiple route patterns to coexist
Advanced Features
Middleware Support
Add multiple handlers to the same route to create middleware chains:
const router = new RadixTree<(req: any, res: any, next?: () => void) => void>();
// Add middleware handlers to the same route
router.insert('GET', '/api/users', authMiddleware);
router.insert('GET', '/api/users', loggingMiddleware);
router.insert('GET', '/api/users', usersHandler);
const result = router.match('GET', '/api/users');
if (result) {
const [handlers, paramMap, params] = result;
// Execute all handlers in sequence
for (const handler of handlers) {
await handler(req, res);
}
// Or execute with next() pattern
let idx = 0;
const next = () => {
if (idx < handlers.length) {
handlers[idx++](req, res, next);
}
};
next();
}Route Constraints
Use regex patterns with {} syntax to validate parameters:
// Only match numeric IDs
router.insert('GET', '/users/:id{\\d+}', handler);
// Match email addresses
router.insert(
'GET',
'/user/:email{[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}}',
emailHandler,
);
// UUID pattern
router.insert(
'GET',
'/resource/:uuid{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}',
uuidHandler,
);
// Hex color pattern
router.insert('GET', '/color/:hex{#[0-9a-fA-F]{6}}', colorHandler);
// Won't match - returns null
router.match('GET', '/users/abc'); // null (not numeric)
router.match('GET', '/user/invalid'); // null (not email)
router.match('GET', '/color/#GGGGGG'); // null (invalid hex)Why use {} instead of ()?
- Clearer syntax separation between router syntax
{}and regex content - No ambiguity with regex capturing groups
- Simpler parsing and better error messages
- Compatible with Hono-style routing patterns
Multi-Parameter Routes
Define multiple parameters in a single path segment using separators:
Separator Support
✅ Fully Supported:
- Dash (
-) -/time/:hour-:minute→{hour: "14", minute: "30"} - Dot (
.) -/file.:name.:ext→{name: "config", ext: "json"} - Mixed (
-and.) -/api/:a-:b.:c→ 3 params with different separators - With Regex -
/date/:y{\d{4}}-:m{\d{2}}→ Validates while extracting params - With Static Text -
/files/:name{[a-z]+}.min.:ext→ Static text between params - Multiple Params -
/ip/:a.:b.:c.:d→ 4+ params in same segment
❌ Not Supported:
- Underscore (
_), At (@), or any other custom separators - These are treated as part of the parameter name, not separators
Only - (dash) and . (dot) are supported as multi-param separators.
// Dash separator
router.insert('GET', '/time/:hour-:minute', timeHandler);
router.match('GET', '/time/14-30');
// Returns: [timeHandler, {hour: 0, minute: 1}, ['14', '30']]
// Dot separator
router.insert('GET', '/file.:name.:ext', fileHandler);
router.match('GET', '/file.config.json');
// Returns: [fileHandler, {name: 0, ext: 1}, ['config', 'json']]
// Multiple params in same segment
router.insert('GET', '/date/:year-:month-:day', dateHandler);
router.match('GET', '/date/2024-12-28');
// Returns: [dateHandler, {year: 0, month: 1, day: 2}, ['2024', '12', '28']]
// Mixed separators (dash and dot)
router.insert('GET', '/api/:version.:endpoint-:id', apiHandler);
router.match('GET', '/api/v2.users-123');
// Returns: [apiHandler, {version: 0, endpoint: 1, id: 2}, ['v2', 'users', '123']]With Regex Constraints
Combine multi-params with regex validation:
// Date format validation
router.insert(
'GET',
'/date/:year{\\d{4}}-:month{\\d{2}}-:day{\\d{2}}',
handler,
);
router.match('GET', '/date/2024-12-28'); // ✅ matches
router.match('GET', '/date/24-12-28'); // ❌ null (year must be 4 digits)
// Version numbers
router.insert(
'GET',
'/v/:major{\\d+}.:minor{\\d+}.:patch{\\d+}',
versionHandler,
);
router.match('GET', '/v/1.2.3'); // ✅ matches
// IP address
router.insert(
'GET',
'/ip/:a{\\d{1,3}}.:b{\\d{1,3}}.:c{\\d{1,3}}.:d{\\d{1,3}}',
ipHandler,
);
router.match('GET', '/ip/192.168.1.1'); // ✅ matchesWith Static Text
Mix static text with multi-params:
// Static text between params
router.insert('GET', '/files/:name{[a-z]+}.min.:ext', handler);
router.match('GET', '/files/app.min.js'); // ✅ {name: 'app', ext: 'js'}
router.match('GET', '/files/app.js'); // ❌ null (missing .min.)
// Image dimensions
router.insert('GET', '/img-:width{\\d+}x:height{\\d+}.png', imgHandler);
router.match('GET', '/img-800x600.png'); // ✅ {width: '800', height: '600'}
// API versioning with prefix
router.insert('GET', '/api/v:version.:endpoint', apiHandler);
router.match('GET', '/api/v2.users'); // ✅ {version: '2', endpoint: 'users'}Limitations
- Other separators not supported:
_,@, or custom characters won't work as multi-param separators - Example:
/user/:first_:lasttreatsfirst_:lastas a single parameter name, not two params - Workaround: Use separate segments:
/user/:first/:lastor supported separators:/user/:first-:last
ALL Method Support
Handle all HTTP methods with a single route:
router.insert('ALL', '/api/health', healthCheck);
// Matches any method
router.match('GET', '/api/health'); // ✅ matches
router.match('POST', '/api/health'); // ✅ matches
router.match('DELETE', '/api/health'); // ✅ matches
// Specific methods override ALL
router.insert('ALL', '/api/users', allHandler);
router.insert('GET', '/api/users', getHandler);
router.match('GET', '/api/users'); // Returns getHandler
router.match('POST', '/api/users'); // Returns allHandlerTypeScript
Full TypeScript support with generics:
type Handler = (req: Request, res: Response) => void;
const router = new RadixTree<Handler>();
router.insert('GET', '/users/:id', (req, res) => {
// Fully typed
});Error Handling
The router returns null when no route matches:
const result = router.match('GET', '/unknown');
if (!result) {
// Route not found
console.log('404 Not Found');
return;
}
const [handlers, paramMap, params] = result;
// Handle the request...Regex Pattern Validation
Routes with regex patterns only match valid formats:
const router = new RadixTree();
router.insert('GET', '/users/:id{\\d+}', handler);
// Invalid format - returns null
const r1 = router.match('GET', '/users/abc');
console.log(r1); // null
// Valid - returns handlers
const r2 = router.match('GET', '/users/123');
console.log(r2); // [[handler], {id: 0}, ['123']]Complex Pattern Examples
// Date pattern (YYYY-MM-DD)
router.insert(
'GET',
'/date/:year{\\d{4}}-:month{\\d{2}}-:day{\\d{2}}',
dateHandler,
);
router.match('GET', '/date/2024-12-25'); // ✅ matches
// Semantic version (X.Y.Z)
router.insert('GET', '/version/:semver{\\d+\\.\\d+\\.\\d+}', versionHandler);
router.match('GET', '/version/1.2.3'); // ✅ matches
// IP address
router.insert('GET', '/ip/:addr{(?:\\d{1,3}\\.){3}\\d{1,3}}', ipHandler);
router.match('GET', '/ip/192.168.1.1'); // ✅ matches
// File extension with alternation
router.insert('GET', '/file/:name.:ext{json|xml|txt}', fileHandler);
router.match('GET', '/file/config.json'); // ✅ matches
router.match('GET', '/file/data.pdf'); // ❌ nullDebugging
Print the router tree to visualize route structure:
const router = new RadixTree();
// Add some routes
router.insert('GET', '/users', handler);
router.insert('GET', '/about', handler);
router.insert('GET', '/users/:id', handler);
router.insert('POST', '/users/:id/posts', handler);
router.insert('GET', '/static/*', handler);
// Print to console
router.printTree();
// Or get as string
const tree = router.printTree(false);
console.log(tree);Output:
┌─ Static Routes (Map)
│ /users [GET]
│ /about [GET]
│
└─ Dynamic Routes (Tree)
<root>
├─ users/
│ └─ :id [GET]
│ └─ /posts [POST]
└─ static/
└─ * [GET]What's Different from find-my-way?
While both routers use radix trees, there are key architectural differences:
| Feature | RadixTree | find-my-way | | ------------------ | ------------------------------- | --------------------------------- | | Tree Structure | Single tree (path-first) | Multiple trees (method-first) | | Performance | Up to 16x faster | Baseline | | Optimization | Pre-compiled regex + Map lookup | Loop-based matching | | Middleware Support | Multiple handlers per route | Single handler per route | | Route Constraints | Regex patterns with {} syntax | Built-in constraints & versioning |
Migration from find-my-way:
// find-my-way
const router = FindMyWay();
router.on('GET', '/users/:id', (req, res, params) => {
console.log(params.id);
});
const match = router.find('GET', '/users/123');
// RadixTree
const router = new RadixTree();
router.insert('GET', '/users/:id', (req, res) => {});
const result = router.match('GET', '/users/123');
if (result) {
const [handlers, paramMap, params] = result;
console.log(params[paramMap.id]);
handlers[0](req, res);
}Contributing
Contributions welcome! Please open an issue or PR.
License
MIT
Credits
Inspired by and compared against:
- find-my-way - Multi-tree method-first router
- koa-tree-router - Koa radix tree router
- hono - Ultrafast web framework
