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

@b9g/router

v0.2.0

Published

Universal request router built on web standards with generator-based middleware.

Readme

@b9g/router

Universal request router built on web standards with generator-based middleware.

Features

  • Web Standards: Built on URLPattern-like syntax, Request, and Response APIs
  • Generator Middleware: Uses yield for flow control (no Express-style next())
  • Universal: Same code runs in browsers, Node.js, Bun, and edge platforms
  • Simple Context: Route parameters and middleware-extensible context
  • Router Composition: Mount subrouters with path prefixes

Installation

npm install @b9g/router

Quick Start

import {Router} from '@b9g/router';

const router = new Router();

// Simple route
router.route('/hello').get(() => new Response('Hello World!'));

// Route with parameters
router.route('/posts/:id').get((request, context) => {
  const {id} = context.params;
  return Response.json({id, title: `Post ${id}`});
});

// Handle request
const response = await router.handle(request);

Middleware

The router supports function and generator-based middleware with yield for clean flow control:

// Function middleware
router.use(async (request, context) => {
  if (!request.headers.get('Authorization')) {
    return new Response('Unauthorized', { status: 401 });
  }
  // Return null/undefined to continue to next middleware
  return null;
});

// Generator middleware
router.use(async function* (request, context) {
  console.log(`${request.method} ${request.url}`);
  const response = yield request;
  console.log(`${response.status}`);
  return response;
});

API Reference

Router

Constructor

new Router()

Methods

route(pattern)

Create a route builder for the given pattern.

router.route('/api/posts/:id')
  .get(handler)
  .post(handler)
  .delete(handler);
use(middleware)

Add global middleware.

router.use(loggingMiddleware);
handle(request): Promise<Response>

Handle an incoming request and return a response.

const response = await router.handle(request);
mount(path, subrouter)

Mount a subrouter at a specific path prefix.

const apiRouter = new Router();
apiRouter.route('/users').get(handler);

const mainRouter = new Router();
mainRouter.mount('/api/v1', apiRouter);
// Routes become: /api/v1/users
match(url): RouteMatch | null

Match a URL against registered routes without executing handlers.

const match = router.match(new URL('https://example.com/api/users'));
if (match) {
  console.log(match.params, match.methods);
}

Properties

routes: RouteEntry[]

Read-only array of registered routes for introspection.

router.routes.forEach(route => {
  console.log(route.pattern, route.method);
});
middlewares: MiddlewareEntry[]

Read-only array of registered middleware for introspection.

router.middlewares.forEach(mw => {
  console.log(mw.pathPrefix);
});

Context Object

Handler and middleware functions receive a context object:

{
  params: Record<string, string>,    // URL parameters
  // Middleware can add arbitrary properties
}

Middleware can extend context with custom properties:

router.use(async function* (request, context) {
  context.user = await authenticate(request);
  return yield request;
});

Examples

Basic API Router

const router = new Router();

router.route('/api/health').get(() =>
  Response.json({status: 'ok'})
);

router.route('/api/posts')
  .get(async () => {
    const posts = await db.posts.findAll();
    return Response.json(posts);
  })
  .post(async (request) => {
    const data = await request.json();
    const post = await db.posts.create(data);
    return Response.json(post, {status: 201});
  });

Authentication Middleware

const router = new Router();

// Add user to context
router.use(async function* (request, context) {
  const token = request.headers.get('Authorization')?.replace('Bearer ', '');
  if (token) {
    context.user = await verifyToken(token);
  }
  return yield request;
});

// Protected route
router.route('/api/profile').get(async (request, context) => {
  if (!context.user) {
    return new Response('Unauthorized', { status: 401 });
  }
  return Response.json(context.user);
});

Subrouter Mounting

// API subrouter
const apiRouter = new Router();
apiRouter.route('/users').get(getUsersHandler);
apiRouter.route('/posts').get(getPostsHandler);

// Main router
const mainRouter = new Router();
mainRouter.mount('/api/v1', apiRouter);
// Routes become: /api/v1/users, /api/v1/posts

Exports

Classes

  • Router - Main router class
  • RouteBuilder - Fluent API for defining routes (returned by router.route())

Types

// Handler and middleware types
type Handler = (request: Request, context: RouteContext) => Response | Promise<Response>
type FunctionMiddleware = (request: Request, context: RouteContext) => Response | null | undefined | Promise<Response | null | undefined>
type GeneratorMiddleware = (request: Request, context: RouteContext) => Generator<Request, Response | null | undefined, Response> | AsyncGenerator<Request, Response | null | undefined, Response>
type Middleware = GeneratorMiddleware | FunctionMiddleware

// Context and route types
interface RouteContext {
  params: Record<string, string>
}

interface RouteOptions {
  name?: string
}

interface RouteMatch {
  params: Record<string, string>
  methods: string[]
}

interface RouteEntry {
  pattern: MatchPattern
  method: string
  handler: Handler
  name?: string
  middleware: Middleware[]
}

interface MiddlewareEntry {
  middleware: Middleware
  pathPrefix?: string
}

// HTTP methods
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS"

// Utility types
type TrailingSlashMode = "strip" | "add"

Middleware Utilities

Standard middleware is available from @b9g/router/middleware:

import {cors, trailingSlash} from '@b9g/router/middleware';

// CORS middleware
router.use(cors({
  origin: "https://example.com",
  credentials: true
}));

// Trailing slash normalization
router.use(trailingSlash("strip")); // /path/ → /path

Available Middleware

cors(options?: CORSOptions)

Handles Cross-Origin Resource Sharing headers and preflight requests.

interface CORSOptions {
  origin?: string | string[] | ((origin: string) => boolean);  // Default: "*"
  methods?: string[];  // Default: ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"]
  allowedHeaders?: string[];  // Default: ["Content-Type", "Authorization"]
  exposedHeaders?: string[];
  credentials?: boolean;  // Default: false
  maxAge?: number;  // Default: 86400 (24 hours)
}

trailingSlash(mode: TrailingSlashMode)

Normalizes URL trailing slashes via 301 redirect.

type TrailingSlashMode = "strip" | "add";

License

MIT