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

@malobre/bihan

v0.3.0

Published

A tiny, type-safe router built on URLPattern with zero dependencies.

Readme

🤏 Bihan

A tiny, type-safe router built on URLPattern with zero dependencies.

⚠️ Experimental Package

This package is experimental and under active development. Routing performance is sub-optimal (routes are matched linearly). The primary goal of this experiment is to develop a type-safe routing API.

Installation

npm install @malobre/bihan

Usage

import { route } from '@malobre/bihan';

await route(
  ({ on }) => [
    // Simple route returning data
    on('GET', '/health').pipe(() => ({ status: 'ok' })),

    // Route with path parameters
    on('GET', '/users/:id').pipe((ctx) => {
      const userId = ctx.urlPatternResult.pathname.groups['id'];
      return Response.json({ userId });
    }),

    // Middleware with context augmentation
    on('GET', '/api/user')
      .pipe((ctx) => ctx.with({user: "John"}))
      .pipe((ctx) => {
        // ctx.user is properly typed
        return Response.json({ message: `Hello ${ctx.user}` });
      }),
  ],
  request
);

API

route(createRoutes, request, ctxData?)

Routes an incoming request to the first matching handler.

Parameters:

  • createRoutes - Factory function that returns an array of routes
  • request - The incoming Request object
  • ctxData - Optional initial context data available to all handlers

Returns: The handler result, or undefined if no route matched.

Behavior:

  • Routes are matched in order - first match wins
  • Errors from handlers propagate to the caller

on(method, pattern)

Registers a route and returns a pipe builder.

Parameters:

  • method - an HTTP method, an array of methods, or AnyMethod symbol
  • pattern - URL pattern as:
    • String (interpreted as pathname): '/users/:id'
    • URLPattern object: new URLPattern({ pathname: '/users/:id' })
    • URLPatternInit: { pathname: '/users/:id', search: '*' }

Returns: A pipe builder with a .pipe(handler) method for adding handlers.

Pipe Behavior:

Handlers are added using .pipe(handler) and receive a Context<T>. They can return:

  • Context object - pass the context to the next handler
  • undefined - keep the current context for the next handler
  • NoMatch - tells the router to try other routes
  • Any other value - Terminates the route and returns that value

Best Practices

Route Organization

Group related routes together and order from most specific to least specific:

await route(({ on }) => [
  // Specific routes first
  on('GET', '/api/users/:id').pipe(getUserById),
  on('POST', '/api/users').pipe(createUser),

  // Wildcards last
  on('GET', '/api/*').pipe(catchAllApi),
], request);

Reusable Middleware

Create composable middleware by defining handler functions:

import type { Context } from '@malobre/bihan';

// Validation middleware - generic over context type
const validateBody = async <TCtxData>(ctx: Context<TCtxData>) => {
  const body = await ctx.request.json();

  if (!body.name) {
    return Response.json({ error: 'Name required' }, { status: 400 });
  }

  return ctx.with({ body });
};

Advanced Features

Custom Context Data

Pass initial context to all handlers via the third parameter:

const appContext = {
  db: database,
  config: appConfig,
};

await route(
  ({ on }) => [
    on('GET', '/users').pipe(async (ctx) => {
      // ctx.db and ctx.config are available and properly typed
      const users = await ctx.db.query('SELECT * FROM users');
      return Response.json(users);
    }),
  ],
  request,
  appContext
);

Full URLPattern Support

Use any URLPattern features, not just pathname:

on('GET', {
  pathname: '/api/:version/*',
  search: 'key=:apiKey',
}).pipe((ctx) => {
  const { version } = ctx.urlPatternResult.pathname.groups;
  const { apiKey } = ctx.urlPatternResult.search.groups;
  return Response.json({ version, apiKey });
})

// Or use URLPattern directly
const pattern = new URLPattern({
  protocol: 'https',
  hostname: 'api.example.com',
  pathname: '/v:version/*',
});

on('GET', pattern).pipe((ctx) => {
  // Full control over matching
})

Helper Utilities

Bihan provides helper utilities for common middleware tasks:

withHeader

Validates that a header is present or has a specific value:

import { withHeader } from '@malobre/bihan/with-header.js';

on('POST', '/api/data')
  .pipe(withHeader('Content-Type', 'application/json'))
  .pipe((ctx) => {
    // Content-Type is validated
    return Response.json({ success: true });
  })

// Or just check for presence
on('POST', '/api/data')
  .pipe(withHeader('X-API-Key'))
  .pipe((ctx) => {
    // X-API-Key header is present
    return Response.json({ success: true });
  })

withContentType

Validates the Content-Type header (case-insensitive):

import { withContentType } from '@malobre/bihan/with-content-type.js';

on('POST', '/api/data')
  .pipe(withContentType('application/json'))
  .pipe((ctx) => {
    // Content-Type is validated
    return Response.json({ success: true });
  })

withAuthorization

Parses and validates the Authorization header:

import { withAuthorization } from '@malobre/bihan/with-authorization.js';

on('GET', '/api/protected')
  .pipe(withAuthorization((value, ctx) => {
    if (value === null) {
      return new Response('missing `Authorization` header', { status: 401 });
    }

    const { scheme, credentials } = value;

    if (scheme !== 'Bearer' || !isValidToken(credentials)) {
      return new Response('Invalid token', { status: 401 });
    }
    return ctx.with({ token: credentials });
  }))
  .pipe((ctx) => {
    // ctx.token is available
    return Response.json({ data: 'protected' });
  })