A tiny, type-safe router built on URLPattern with zero dependencies.
⚠️ Experimental PackageThis 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.
npm install @malobre/bihanimport { 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
);In bihan, routes are chains of functions, we call them "pipes".
Functions in a pipe are called handlers, they take a single Context parameter.
Context are objects, they provide a single with(data) function which return a new Context augmented with data
The first handler in a pipe will receive a Context<RouteIntrinsics> which contains the HTTP request in its request field.
Handlers can return any value, but some have specific effects:
- a
Contextobject - will be fed into the next handler undefined- keep the current context for the next handlerNoMatch- tells the router to try other routes- Any other value - Terminates the route and returns that value
Routes an incoming request to the first matching handler.
Parameters:
createRoutes({ createPipe, on })- Factory function that returns an array of routesrequest- The incomingRequestobjectctxData- 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
A shorthand for createPipe().pipe(filterMethod(method)).pipe(filterURLPattern(pattern)).
Registers a route and returns a pipe builder.
Parameters:
method- an HTTP method, an array of methods, orAnyMethodsymbolpattern- URL pattern as:- String (interpreted as pathname):
'/users/:id' - URLPattern object:
new URLPattern({ pathname: '/users/:id' }) - URLPatternInit:
{ pathname: '/users/:id', search: '*' }
- String (interpreted as pathname):
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:
Contextobject - pass the context to the next handlerundefined- keep the current context for the next handlerNoMatch- tells the router to try other routes- Any other value - Terminates the route and returns that value
Used when more advanced filtering is needed, return an empty pipe, which will resolve to undefined if called.
By convention, handlers that could return NoMatch are prefixed by filter.
Handlers that are not final, i.e. could return a Context, are prefixed by with.
filterMethodfilterURLPattern
withHeaderwithHeaderFnwithContentTypewithAuthorization
Create composable handlers by making them generic over the context data:
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 });
};