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

@vyriy/router

v0.8.9

Published

Router utility for Vyriy projects

Readme

@vyriy/router

Part of Vyriy - a calm architecture toolkit for TypeScript, React, SSR, SSG, APIs, and cloud-ready apps.

Full documentation: https://vyriy.dev/docs/router/

Router utility for Vyriy projects.

Purpose

This package provides a small router for Lambda handlers and native Node HTTP servers:

  • createRouter() for API Gateway events
  • createStreamRouter() for Lambda response streaming
  • createHttpRouter() for native Node IncomingMessage/ServerResponse handlers

It is intentionally kept small:

  • matches by HTTP method and exact path through an O(1) hash lookup
  • matches :param segments (for example /api/tenants/:tenantId) and exposes the captured values
  • matches any HTTP method on a path through all(path, handler)
  • passes API Gateway event data into handlers (or raw Node request/response for the HTTP router)
  • returns a Lambda-friendly response shape (the stream and HTTP routers write the response instead)

Matching stays fast: exact static paths are resolved first through the hash lookup, and :param routes are only scanned when that lookup misses, so static traffic never pays for parameter matching. Resolution runs from the most specific match to the most general: exact path, dynamic path, all path, fallback. Parameter matching is segment-based (no regular expressions), and static segments win over :param segments from left to right.

It still does not try to be a general dispatcher. If you need regular expressions, wildcards, or more advanced dispatch rules, that logic should live in a separate package or layer.

Install

With npm:

npm install @vyriy/router

With Yarn:

yarn add @vyriy/router

Basic Router

import { createRouter } from '@vyriy/router';

const router = createRouter();

router.get('/health', async ({ event, query, headers, pathParameters, body }) => ({
  body: JSON.stringify({
    ok: true,
    method: event.httpMethod,
    query,
    headers,
    pathParameters,
    body,
  }),
}));

router.fallback(async ({ event }) => ({
  statusCode: 404,
  body: JSON.stringify({
    message: 'Not Found',
    path: event.path,
  }),
}));

export const handler = router.handle();

Route Parameters

Any segment that starts with : is a named parameter. Captured values are merged into pathParameters (alongside any values API Gateway already provided), and captured values win on key conflicts:

router.get('/api/tenants/:tenantId/runs/:runId', async ({ pathParameters }) => ({
  body: JSON.stringify({
    tenantId: pathParameters?.tenantId,
    runId: pathParameters?.runId,
  }),
}));

Values are URL-decoded (/users/john%20doe yields john doe). When two dynamic routes could match the same path, the one whose static segment appears earlier from left to right wins, so /a/b/:y is preferred over /a/:x/c for /a/b/c. Registering two routes that resolve to the same pattern (such as /a/:x and /a/:y), reusing a parameter name, or using an empty : name throws at registration time.

The native HTTP router exposes captured values on request.params instead, since its handlers receive the raw request and response:

import type { RequestWithParams } from '@vyriy/router/http';

httpRouter.get('/api/tenants/:tenantId', (request, response) => {
  const { tenantId } = (request as RequestWithParams).params ?? {};

  response.writeHead(200).end(JSON.stringify({ tenantId }));
});

All Methods

Every variant supports all(path, handler) for routes that accept any HTTP method, which suits transports that multiplex methods on one path (such as MCP Streamable HTTP). Method-specific routes take precedence over all routes on the same path, and all paths may use :param segments too:

router.all('/webhook/:provider', async ({ pathParameters, event }) => ({
  body: JSON.stringify({
    provider: pathParameters?.provider,
    method: event.httpMethod,
  }),
}));

Stream Router

Use the streaming router when handlers write directly to a Lambda response stream. The stream is passed as the second handler argument, and stream handlers do not return a response object.

Import it from the @vyriy/router/stream subpath (clean createRouter/router names), or use the aggregated createStreamRouter alias from the root entry.

import { createRouter as createStreamRouter } from '@vyriy/router/stream';

const streamRouter = createStreamRouter();

streamRouter.get('/events', ({ event, query }, responseStream) => {
  responseStream.setContentType?.('text/plain');
  responseStream.write(`path: ${event.path}\n`);
  responseStream.write(`cursor: ${query?.cursor ?? 'start'}\n`);
  responseStream.end('done');
});

streamRouter.fallback(({ event }, responseStream) => {
  responseStream.setContentType?.('application/json');
  responseStream.end(
    JSON.stringify({
      message: 'Not Found',
      path: event.path,
    }),
  );
});

export const handler = streamRouter.handle();

HTTP Router

Use createHttpRouter() when handlers work directly with native Node IncomingMessage and ServerResponse objects. Handlers own the response lifecycle and write the response themselves, which fits transports such as MCP Streamable HTTP.

In addition to the method helpers, the HTTP router supports all(path, handler) for routes that accept any HTTP method. Method-specific routes take precedence over all routes on the same path.

Import it from the @vyriy/router/http subpath (clean createRouter/router names), or use the aggregated createHttpRouter alias from the root entry.

import { createRouter as createHttpRouter } from '@vyriy/router/http';

const httpRouter = createHttpRouter();

httpRouter.get('/health', (request, response) => {
  response
    .writeHead(200, {
      'content-type': 'application/json',
    })
    .end(JSON.stringify({ ok: true }));
});

// transports that handle multiple HTTP methods on one path, such as MCP Streamable HTTP
httpRouter.all('/mcp', async (request, response) => {
  await transport.handleRequest(request, response);
});

httpRouter.fallback((request, response) => {
  response
    .writeHead(404, {
      'content-type': 'application/json',
    })
    .end(
      JSON.stringify({
        message: 'Not Found',
        path: request.url,
      }),
    );
});

export const handler = httpRouter.handle();

Calm Composition

The router keeps request matching separate from handler wrappers and local server adapters. A small API can stay as a plain composition of focused packages:

import { api } from '@vyriy/handler';
import { createRouter } from '@vyriy/router';
import { server } from '@vyriy/server';

const router = createRouter();

router.get('/health', () => ({
  body: JSON.stringify({
    ok: true,
  }),
}));

const handler = api(router.handle());

server(handler);

The same shape works for Lambda response streaming:

import { streamApi } from '@vyriy/handler';
import { createStreamRouter } from '@vyriy/router';
import { streamServer } from '@vyriy/server';

const router = createStreamRouter();

router.get('/events', (_params, responseStream) => {
  responseStream.setContentType?.('text/plain');
  responseStream.end('ok');
});

const handler = streamApi(router.handle());

streamServer(handler);

And for native Node HTTP handlers:

import { httpApi } from '@vyriy/handler';
import { createHttpRouter } from '@vyriy/router';
import { httpServer } from '@vyriy/server';

const router = createHttpRouter();

router.get('/health', (request, response) => {
  response
    .writeHead(200, {
      'content-type': 'application/json',
    })
    .end(JSON.stringify({ ok: true }));
});

const handler = httpApi(router.handle());

httpServer(handler);

For a Lambda-only entrypoint, keep the same composition and export the handler:

import { api } from '@vyriy/handler';
import { createRouter } from '@vyriy/router';

const router = createRouter();

router.get('/health', () => ({
  body: JSON.stringify({
    ok: true,
  }),
}));

export const handler = api(router.handle());

Exports

The classic Lambda router lives at the root entry, while the streaming and native HTTP variants each have their own subpath with clean, unprefixed names:

import { createRouter, router } from '@vyriy/router';
import { createRouter, router } from '@vyriy/router/http';
import { createRouter, router } from '@vyriy/router/stream';

The root entry also re-exports every variant under prefixed names, so a single import keeps working:

import { createHttpRouter, createRouter, createStreamRouter } from '@vyriy/router';
import { HttpRouter, Router, StreamRouter } from '@vyriy/router';

The low-level Router classes are exported from each entry: Router from @vyriy/router, @vyriy/router/http, and @vyriy/router/stream, plus the aggregated HttpRouter and StreamRouter names from the root entry.

API

  • createRouter() returns a chainable router API.
  • createStreamRouter() returns a chainable response streaming router API.
  • createHttpRouter() returns a chainable native HTTP router API.
  • Paths may contain :param segments on every method helper, all, and on.
  • router.all(path, handler) registers a handler for any HTTP method on a path.
  • router.get(path, handler) registers a GET handler.
  • router.post(path, handler) registers a POST handler.
  • router.put(path, handler) registers a PUT handler.
  • router.delete(path, handler) registers a DELETE handler.
  • router.patch(path, handler) registers a PATCH handler.
  • router.fallback(handler) registers a handler for unmatched requests.
  • router.handle() returns (event) => router.route(event) for API Gateway wrappers.
  • router.route(event) resolves the matching route and returns an API Gateway response.
  • streamRouter.all(path, handler) registers a stream handler for any HTTP method on a path.
  • streamRouter.handle() returns (event, responseStream) => streamRouter.route(event, responseStream) for stream wrappers.
  • streamRouter.route(event, responseStream) resolves the matching route and writes to the stream.
  • httpRouter.all(path, handler) registers a handler for any HTTP method on an exact path.
  • httpRouter.handle() returns (request, response) => httpRouter.route(request, response) for native HTTP wrappers.
  • httpRouter.route(request, response) resolves the matching route and lets the handler write the response.

Route handlers may omit statusCode; the router normalizes missing status codes to 200 before returning from router.route(event).

The low-level Router, StreamRouter, and HttpRouter classes are also available from @vyriy/router (each variant additionally exports its class as Router from its own subpath):

  • router.on(method, path, handler)
  • router.all(path, handler)
  • router.fallback(handler)
  • router.route(event)
  • streamRouter.on(method, path, handler)
  • streamRouter.all(path, handler)
  • streamRouter.fallback(handler)
  • streamRouter.route(event, responseStream)
  • httpRouter.on(method, path, handler)
  • httpRouter.all(path, handler)
  • httpRouter.fallback(handler)
  • httpRouter.route(request, response)

Route handlers receive:

type HandlerParams = {
  query?: APIGatewayProxyEventQueryStringParameters;
  body?: string;
  headers?: APIGatewayProxyEvent['headers'];
  pathParameters?: APIGatewayProxyEvent['pathParameters'];
  event: APIGatewayProxyEvent;
};

Stream route handlers receive the same HandlerParams as the first argument and ResponseStream as the second argument:

type StreamHandler = (params: HandlerParams, responseStream: ResponseStream) => void | Promise<void>;

HTTP route handlers receive the native Node request and response objects directly:

type HttpHandler = (request: IncomingMessage, response: ServerResponse) => void | Promise<void>;

When no HTTP route matches and no fallback is registered, the HTTP router writes a JSON 404 response itself.