A tiny, type-safe router built on URLPattern with zero dependencies.
npm install @malobre/bihanA 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.
``bash`
npm install @malobre/bihan
`typescript
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
);
Routes an incoming request to the first matching handler.
Parameters:
- createRoutes - Factory function that returns an array of routesrequest
- - The incoming Request objectctxData
- - 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 symbolpattern
- - URL pattern as:'/users/:id'
- String (interpreted as pathname): new URLPattern({ pathname: '/users/:id' })
- URLPattern object: { pathname: '/users/:id', search: '*' }
- URLPatternInit:
Returns: A pipe builder with a .pipe(handler) method for adding handlers.
Pipe Behavior:
Handlers are added using .pipe(handler) and receive a Context. 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
Group related routes together and order from most specific to least specific:
`typescript
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);
`
Create composable middleware by defining handler functions:
`typescript
import type { Context } from '@malobre/bihan';
// Validation middleware - generic over context type
const validateBody = async
const body = await ctx.request.json();
if (!body.name) {
return Response.json({ error: 'Name required' }, { status: 400 });
}
return ctx.with({ body });
};
`
Pass initial context to all handlers via the third parameter:
`typescript
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
);
`
Use any URLPattern features, not just pathname:
`typescript
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
})
`
Bihan provides helper utilities for common middleware tasks:
#### withHeader
Validates that a header is present or has a specific value:
`typescript
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):
`typescript
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:
`typescript
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' });
})
``