npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nextrush/router

v3.0.6

Published

High-performance radix tree router for NextRush

Readme

@nextrush/router

High-performance radix tree router for NextRush. O(k) route matching where k = path length, not route count.

The Problem

Traditional array-based routers iterate through all routes to find a match. With 1,000 routes, that's 1,000 comparisons per request. Route order matters. Performance degrades linearly.

How NextRush Approaches This

The router uses a radix tree (compressed prefix tree):

  • Routes share common prefixes in the tree structure
  • Matching is O(k) where k is path length (typically 10-50 characters)
  • Route count doesn't affect matching speed
  • Memory efficient: shared prefixes stored once

Mental Model

Routes:
  /users
  /users/:id
  /users/:id/posts
  /products
  /products/:id

Tree:
  /
  ├── users
  │   └── /:id
  │       └── /posts
  └── products
      └── /:id

When matching /users/123/posts:

  1. Match / → found
  2. Match users → found
  3. Match /:id → captures 123
  4. Match /posts → found
  5. Return handler + params

Installation

pnpm add @nextrush/router

Quick Start

import { createApp } from '@nextrush/core';
import { createRouter } from '@nextrush/router';

const app = createApp();
const router = createRouter();

router.get('/', (ctx) => {
  ctx.json({ message: 'Home' });
});

router.get('/users/:id', (ctx) => {
  ctx.json({ userId: ctx.params.id });
});

app.route('/', router);
app.listen(3000);

Features

  • Radix Tree Algorithm: O(k) lookup where k = path length
  • Route Parameters: Named parameters with :param syntax
  • Wildcard Routes: Catch-all with * parameter
  • Route Groups: Prefix-based grouping with middleware
  • Method-Based Routing: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS
  • Redirects: Built-in redirect support with parameter interpolation
  • Zero Dependencies: Only depends on @nextrush/types

Route Patterns

Static Routes

router.get('/api/users', handler);
router.get('/api/posts', handler);
router.get('/health', handler);

Named Parameters

// Single parameter
router.get('/users/:id', (ctx) => {
  ctx.json({ id: ctx.params.id });
});

// Multiple parameters
router.get('/users/:userId/posts/:postId', (ctx) => {
  const { userId, postId } = ctx.params;
  ctx.json({ userId, postId });
});

Wildcard Routes

// Catch-all: captures everything after /files/
router.get('/files/*', (ctx) => {
  const filePath = ctx.params['*'];
  ctx.json({ path: filePath });
});

// /files/docs/readme.md → ctx.params['*'] = 'docs/readme.md'

Wildcards must be at the end of the route. Everything after the * segment is captured.

HTTP Methods

router.get('/resource', handler);
router.post('/resource', handler);
router.put('/resource/:id', handler);
router.patch('/resource/:id', handler);
router.delete('/resource/:id', handler);
router.head('/resource', handler);
router.options('/resource', handler);

// Register for all standard methods (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)
// TRACE and CONNECT are excluded for security reasons
router.all('/any', handler);

// Register specific method dynamically
router.route('GET', '/dynamic', handler);

Route Groups

Group routes with a common prefix:

const api = createRouter({ prefix: '/api/v1' });

api.get('/users', listUsers); // GET /api/v1/users
api.get('/users/:id', getUser); // GET /api/v1/users/:id
api.post('/users', createUser); // POST /api/v1/users

app.route('/', api);

Nested Groups

const router = createRouter();

router.group('/api', (api) => {
  api.group('/v1', (v1) => {
    v1.get('/users', handler); // GET /api/v1/users
    v1.get('/posts', handler); // GET /api/v1/posts
  });

  api.group('/v2', (v2) => {
    v2.get('/users', handler); // GET /api/v2/users
  });
});

app.route('/', router);

Group Middleware

Apply middleware to all routes in a group:

const auth = async (ctx, next) => {
  if (!ctx.get('Authorization')) {
    ctx.status = 401;
    ctx.json({ error: 'Unauthorized' });
    return;
  }
  await next();
};

router.group('/admin', [auth], (admin) => {
  admin.get('/dashboard', getDashboard);
  admin.get('/settings', getSettings);
});

Route Middleware

Apply middleware to specific routes:

const auth = async (ctx, next) => {
  if (!ctx.get('Authorization')) {
    ctx.status = 401;
    ctx.json({ error: 'Unauthorized' });
    return;
  }
  await next();
};

// Single route with middleware
router.get('/protected', auth, handler);

// Multiple middleware
router.get('/admin', auth, adminOnly, handler);

Middleware executes in order: authadminOnlyhandler

Redirects

Built-in support for HTTP redirects:

// Permanent redirect (301)
router.redirect('/old-page', '/new-page');

// Temporary redirect (302)
router.redirect('/temp', '/destination', 302);

// Redirect to external URL
router.redirect('/docs', 'https://docs.example.com');

// With parameter interpolation
router.redirect('/users/:id', '/profiles/:id');
// /users/123 → redirects to /profiles/123

Supported status codes: 301, 302, 303, 307, 308

Sub-Router Mounting

Compose routers together using mount() or use():

Using mount() (Recommended)

const userRouter = createRouter();
userRouter.get('/', listUsers);
userRouter.get('/:id', getUser);
userRouter.post('/', createUser);

const postRouter = createRouter();
postRouter.get('/', listPosts);
postRouter.get('/:id', getPost);

const api = createRouter();
api.mount('/users', userRouter); // Clean explicit mounting
api.mount('/posts', postRouter);

app.route('/api', api);
// Results in: /api/users, /api/users/:id, /api/posts, /api/posts/:id

Using use() (Classic Pattern)

The traditional use() method also works:

const api = createRouter();
api.use('/users', userRouter);
api.use('/posts', postRouter);

app.route('/api', api);
// Or classic: app.use(api.routes());

Direct App Mounting (Hono-Style)

For the cleanest DX, mount routers directly on the app:

import { createApp } from '@nextrush/core';

const app = createApp();
app.route('/users', userRouter); // No routes() call needed!
app.route('/posts', postRouter);

Allowed Methods

Automatically handle 405 Method Not Allowed:

app.route('/', router);
app.use(router.allowedMethods());

// GET /users → 200 OK (if GET handler exists)
// POST /users (no POST handler) → 405 Method Not Allowed
// OPTIONS /users → Returns allowed methods in Allow header

Router Options

interface RouterOptions {
  // Route prefix prepended to all routes
  prefix?: string;

  // Case sensitive matching (default: false)
  caseSensitive?: boolean;

  // Strict trailing slash handling (default: false)
  strict?: boolean;
}

const router = createRouter({
  prefix: '/api/v1',
  caseSensitive: false,
  strict: false,
});

Case Sensitivity

// Default: case-insensitive
const router = createRouter();
router.get('/Users', handler);
router.match('GET', '/users'); // ✓ matches

// Case-sensitive mode
const router = createRouter({ caseSensitive: true });
router.get('/Users', handler);
router.match('GET', '/Users'); // ✓ matches
router.match('GET', '/users'); // ✗ no match

Trailing Slash

// Default: trailing slashes are normalized
router.get('/users', handler);
router.match('GET', '/users'); // ✓ matches
router.match('GET', '/users/'); // ✓ matches (normalized)

Performance

The radix tree router provides:

  • O(k) Lookup: Where k is the path length, not route count
  • Memory Efficient: Shared prefixes are stored once
  • Fast Parameter Extraction: Single-pass parsing

Benchmarks

| Routes | Lookup Time | | ------ | ----------- | | 100 | ~0.02ms | | 1,000 | ~0.02ms | | 10,000 | ~0.02ms |

Route count doesn't affect lookup time significantly.

API Reference

createRouter(options?)

Create a new router instance.

function createRouter(options?: RouterOptions): Router;

Parameters:

| Parameter | Type | Required | Default | Description | | --------- | --------------- | -------- | ------- | -------------------- | | options | RouterOptions | No | {} | Router configuration |

Options:

| Option | Type | Default | Description | | --------------- | --------- | ------- | ------------------------------------- | | prefix | string | '' | Path prefix for all routes | | caseSensitive | boolean | false | Enable case-sensitive matching | | strict | boolean | false | Enable strict trailing slash handling |

Returns: Router instance

Router Methods

HTTP Method Shortcuts

router.get(path: string, ...handlers: RouteHandler[]): this
router.post(path: string, ...handlers: RouteHandler[]): this
router.put(path: string, ...handlers: RouteHandler[]): this
router.delete(path: string, ...handlers: RouteHandler[]): this
router.patch(path: string, ...handlers: RouteHandler[]): this
router.head(path: string, ...handlers: RouteHandler[]): this
router.options(path: string, ...handlers: RouteHandler[]): this
router.all(path: string, ...handlers: RouteHandler[]): this
router.route(method: HttpMethod, path: string, ...handlers: RouteHandler[]): this

router.redirect(from, to, status?)

Register a redirect route.

router.redirect(from: string, to: string, status?: 301 | 302 | 303 | 307 | 308): this

Default status is 301 (Moved Permanently). Status codes 307 and 308 register handlers for all standard HTTP methods (to preserve the original method). Other codes register GET and HEAD only.

router.group(prefix, middleware?, callback)

Create a route group with shared prefix and middleware.

router.group(prefix: string, callback: (router: Router) => void): this
router.group(prefix: string, middleware: Middleware[], callback: (router: Router) => void): this

router.use(pathOrMiddleware, routerOrUndefined?)

Mount middleware or sub-router.

router.use(middleware: Middleware): this
router.use(path: string, subRouter: Router): this
router.use(subRouter: Router): this

router.mount(path, subRouter)

Mount a sub-router at a path prefix. Equivalent to use(path, router) with clearer intent.

router.mount(path: string, subRouter: Router): this

Example:

const api = createRouter();
api.mount('/users', userRouter);
api.mount('/posts', postRouter);

router.match(method, path)

Match a route and return handler + params.

router.match(method: HttpMethod, path: string): RouteMatch | null

Returns:

interface RouteMatch {
  handler: RouteHandler;
  params: Record<string, string>;
  middleware: Middleware[];
}

router.reset()

Clear all registered routes, middleware, and internal state. Useful for testing or plugin teardown.

const router = createRouter();
router.get('/users', handler);

router.reset();
// All routes, static route cache, middleware, param/wildcard children are cleared

Calling reset() makes the router reusable — you can register fresh routes after resetting.

router.routes()

Get middleware function to mount on application.

const middleware = router.routes();
app.use(middleware);

router.allowedMethods()

Get middleware that handles 405 responses and OPTIONS requests.

app.use(router.allowedMethods());

TypeScript Types

import { createRouter, Router } from '@nextrush/router';

import type {
  HttpMethod,
  Middleware,
  Route,
  RouteHandler,
  RouteMatch,
  Router as RouterInterface,
  RouterOptions,
} from '@nextrush/router';

Common Patterns

RESTful Resource Routes

const router = createRouter();

// Users resource
router.get('/users', listUsers);
router.post('/users', createUser);
router.get('/users/:id', getUser);
router.put('/users/:id', updateUser);
router.delete('/users/:id', deleteUser);

app.route('/', router);

API Versioning

const v1 = createRouter({ prefix: '/api/v1' });
v1.get('/users', v1UsersHandler);

const v2 = createRouter({ prefix: '/api/v2' });
v2.get('/users', v2UsersHandler);

app.route('/', v1);
app.route('/', v2);

Catch-All for SPA

router.get('/api/*', apiHandler);

// Serve SPA for all other routes
router.get('/*', (ctx) => {
  ctx.html(indexHtml);
});

Common Mistakes

Forgetting to Mount Routes

// ❌ Routes not mounted
const router = createRouter();
router.get('/users', handler);
// Missing: app.route('/api', router)

// ✅ Recommended: Use app.route()
const router = createRouter();
router.get('/users', handler);
app.route('/api', router);

// ✅ Also works: Classic pattern
app.use(router.routes());

Parameter Name & Value Case

// Parameter names preserve their original case from route definition
router.get('/users/:userId', (ctx) => {
  // ✅ Original case is preserved
  console.log(ctx.params.userId);
});

// Parameter values also preserve their original case
// GET /users/JohnDoe → ctx.params.userId === 'JohnDoe' (not 'johndoe')

Wildcard Placement

// ❌ Wildcard not at end (won't work as expected)
router.get('/*/files', handler);

// ✅ Wildcard at end
router.get('/files/*', handler);

Duplicate Route Registration

// ❌ Throws an error — same method + path registered twice
router.get('/users', handler1);
router.get('/users', handler2);
// Error: "Route conflict: GET /users is already registered"

// ✅ Different methods on the same path are fine
router.get('/users', handler1);
router.post('/users', handler2);

Error Behavior

  • Duplicate routes: Registering the same method + path throws immediately at registration time.
  • Unmatched routes: routes() middleware sets ctx.status = 404 and calls next(), allowing downstream middleware like allowedMethods() to respond.
  • Parameter name conflicts: In development, a warning is logged when two routes define different parameter names at the same tree position. The first registered name takes precedence.

License

MIT