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

@bjoernboss/mws

v1.3.0

Published

Modular Web Server — a TypeScript framework for hosting isolated modules behind HTTP/HTTPS+WebSocket endpoints

Readme

MWS - Modular Web Server

TypeScript License

A lightweight TypeScript framework for hosting isolated modules behind HTTP/HTTPS and WebSocket endpoints. Each module owns a subtree in the URL space and is isolated from its siblings. Modules compose into trees for path-based routing, hostname routing, and request interception.

The server integrates various automation features, such as error handling, validations, caching... Further, it contains integrated logging for proper connection tracing and logging.

Installation

Only depends on ws at runtime.

$ npm install @bjoernboss/mws

Requires Node.js 22 or later.

Quick Start

import { Server, ModuleHandler, ClientRequest, Media, addLogger, createConsoleLogger } from "@bjoernboss/mws";

addLogger(createConsoleLogger());

class HelloModule extends ModuleHandler {
	constructor() {
		super('hello');
	}

	protected override async handleRequest(client: ClientRequest): Promise<void> {
		client.respond('Hello, World!', { media: Media.Text });
	}
}

const server = new Server();
server.listen(new HelloModule(), { port: 8080 });

For HTTPS, pass a TLS configuration:

server.listen(new HelloModule(), {
	port: 443,
	tls: { key: './privkey.pem', cert: './fullchain.pem' }
});

Listeners

server.listen() returns a Listener that emits 'listening', 'failed', and 'stopped' events:

const listener = server.listen(handler, { port: 8080 });

listener.on('listening', (address) => console.log(`Listening on port ${address.port}`));
listener.on('failed', (err) => console.error(`Failed: ${err.message}`));
listener.on('stopped', () => console.log('Stopped'));

A serverless listener does not bind to a port. Instead, connections are passed to it manually via listener.handleRequest() and listener.handleUpgrade() (can also be used for other listeners):

const listener = server.listen(handler, { serverless: { secure: false } });

/* forward requests from an external HTTP server */
externalServer.on('request', (req, resp) => listener.handleRequest(req, resp));
externalServer.on('upgrade', (req, sock, head) => listener.handleUpgrade(req, sock, head));

Writing Modules

A module extends ModuleHandler and implements up to three lifecycle hooks:

import { ModuleHandler, ClientRequest, Server, Params } from "@bjoernboss/mws";

export class MyModule extends ModuleHandler {
	constructor() {
		super('my-module');
	}

	/* Called once when first attached to a server (directly or through a parent module) */
	protected override async handleInitialize(server: Server): Promise<void> {
		/* allocate resources, start timers */
	}

	/* Called for every incoming request routed to this module */
	protected override async handleRequest(client: ClientRequest, params?: Params): Promise<void> {
		/* respond to the client */
	}

	/* Called once after the module has stopped and all clients have left;
	   accepted WebSockets are still open and must be closed manually */
	protected override async handleStop(): Promise<void> {
		/* release resources, close WebSockets */
	}
}

Only handleRequest is required. Unhandled requests can be handled by parent modules or receive an automatic 404 Not Found.

Modules form a tree via linkModule(). They only ever see requests relative to their root. Path translation happens automatically when dispatching to children — client paths are rebased relative to each child module's position in the tree. A module can be linked to multiple parents and will only be initialized once, on first attachment to a server.

Header and HTML Patching

Parent modules can register patches that modify outgoing headers or HTML pages before they are sent to the client:

/* called just before headers are sent (in reverse registration order) */
client.patchHeaders((status, headers) => {
	headers['X-Custom'] = 'value';
});

/* called just before an HTML page is finalized (in reverse registration order) */
client.patchHtmlPage(async (page, status, headers) => {
	page.head.push(build.LoadScript('/analytics.js'));
});

Patches are scoped to the current handler context and automatically removed when the handler returns.

Shutdown

server.stop() waits for all active request handlers to complete before shutting down. Long-running handlers must check client.claimed or await client.responded to detect when the connection has been broken, and exit promptly.

Stopping Individual Modules

Calling module.stop() detaches the module and waits for its active clients to drain. By default, a module stops automatically when all its parents unlink it; this can be disabled with module.stopOnDetach(false).

Helper Modules

Factory functions create common module patterns without subclassing:

dispatch — Path Routing

Routes requests to children by longest URL path match. Stops itself once all children have been unlinked.

import { Server, dispatch } from "@bjoernboss/mws";

const server = new Server();
server.listen(dispatch({
	'/api': apiModule,
	'/static': staticModule
}), { port: 8080 });

host — Hostname Routing

Routes requests to children by longest hostname match (supports sub-domain matching).

import { Server, host } from "@bjoernboss/mws";

server.listen(host({
	'api.example.com': apiModule,
	'example.com': mainModule
}), { port: 8080 });

bind — Parameter and Translation Binding

Forwards all requests to a single child handler, optionally injecting params and a path translate map.

import { bind } from "@bjoernboss/mws";

const bound = bind(myModule, {
	params: { role: 'admin' },
	translate: { '/v2': '/' }
});

check — Host and Port Validation

Validates the request hostname and port before forwarding. Responds 404 and kills the connection on mismatch.

import { check } from "@bjoernboss/mws";

const checked = check(myModule, ['localhost', '127.0.0.1'], { port: 8080 });

lambda — Callback-Based Handler

Handles requests via callbacks instead of subclassing, with optional attached child modules.

import { lambda, ClientRequest, Server, AttachedModule } from "@bjoernboss/mws";

const handler = lambda({
	attach: { api: apiModule },
	setup: async function (server: Server, links: Record<string, AttachedModule>) {
		/* runs on first attachment */
	},
	handle: async function (client: ClientRequest, params, links) {
		if (client.isSubPathOf('/api'))
			await links.api.handle(client, { translate: { '/api': '/' } });
		else
			client.respondNotFound();
	},
	stop: async function (links) {
		/* cleanup */
	}
});

Request Handling

ClientRequest automatically manages requests to handle errors, prevent double-responses, and ensure expected HTTP behavior.

Receiving Data

/* as a complete buffer */
const data = await client.receiveAllBuffer(1_000_000);

/* as a decoded string */
const text = await client.receiveAllText('utf-8', 1_000_000);

/* as a readable stream */
const stream = client.receiveData(1_000_000);

/* directly to a file (atomic: writes to temp file, then renames) */
await client.receiveToFile('/uploads/file.bin', 10_000_000);

/* directly to a file (create-only: fails if the file already exists) */
const created = await client.receiveToFile('/uploads/file.bin', 10_000_000, { create: true });

Responding

/* simple text or buffer */
client.respond('OK', { media: Media.Text, status: Status.Ok });

/* streaming response */
const writer = client.respondData({ media: Media.Json, dynamicEncode: true });
writer.end(JSON.stringify(data));

/* file with automatic range requests, etag, last-modified, encoding, and caching */
if (!await client.tryRespondFile('/var/www/index.html'))
	client.respondNotFound();

/* HTML page */
await client.respondHtml(page, { status: Status.Ok });

Important: Streams returned by receiveData() and respondData() can emit 'error' events. Always register an 'error' handler on these streams to prevent unhandled errors from crashing the process. Generally: correspondingly tagged functions may throw or error and must handle errors accordingly, to prevent crashing the process.

Convenience methods: respondOk, respondNotFound, respondBadRequest, respondForbidden, respondInternalError, respondConflict, respondSeeOther, respondTemporaryRedirect, respondPermanentRedirect, respondCreated, and others.

Cache Control

Every response method accepts an optional cache parameter of type CachePolicy to control the Cache-Control header. If Cache-Control is set manually in the headers, the policy is ignored.

| Policy | Default Header | Used By Default In | |---|---|---| | immutable | public, max-age=2592000, immutable | tryRespondFile (versioned paths) | | static | public, no-cache | tryRespondFile (normal files) | | private | private, no-cache | respond, respondData, respondHtml, and all convenience methods | | sensitive | no-cache, no-store, max-age=0, must-revalidate | respondInternalError, respondForbidden | | none | (no header set) | (only when explicitly chosen) |

The default header values for each policy can be customized via ClientConfig.cache:

const server = new Server({
	client: {
		cache: {
			immutable: 'public, max-age=604800, immutable',
			static: 'public, max-age=60',
			private: 'private, no-cache',
			sensitive: 'no-store',
			none: ''
		}
	}
});

WebSocket

protected override async handleRequest(client: ClientRequest): Promise<void> {
	const ws = await client.acceptWebSocket();
	if (ws == null) return;

	ws.on('data', (data: Buffer) => {
		ws.send(data);
	});
	ws.on('close', () => {
		/* cleanup */
	});
}

ClientSocket handles alive checks automatically via configurable ping/pong intervals (ClientConfig.webSocketTimeout, ClientConfig.webSocketAliveTimeout).

Non-upgrade requests that reach acceptWebSocket() receive an automatic 426 Upgrade Required response.

Caching

Server provides integrated caching with a two-layer system:

In-Memory File Cache

An LRU cache for frequently served files. Encoded variants (gzip, brotli, ...) are cached alongside the original.

client.tryRespondFile() reads through this cache automatically.

Immutable Versioned Paths

File paths can be tagged with a unique version identifier so clients can cache them immutably:

/* style.css becomes style.<id>.css — the id changes when the file changes */
const versionedPath = server.cache.immutable('my-module', '/static/style.css');

When a request arrives for an immutable path, the cache strips the version tag, serves the underlying file, and redirects stale version tags to the current one. Set CacheConfig.immutableStatePath to persist version mappings across restarts.

Direct Cache Access

These methods are designed for modules to be used, and allows directly to write to the disk, and update the cache at the same time. May throw exceptions.

const buffer = await server.cache.read('/path/to/data.json');
await server.cache.write('/path/to/output.json', JSON.stringify(data));
await server.cache.write('/path/to/output.bin', readableStream);
await server.cache.remove('/path/to/old.json');
server.cache.flush();

HTML Building

The build namespace provides programmatic HTML construction with automatic escaping. It allows parent modules to use the patchHtmlPage function, to build around the HTML, before serving it:

import { build } from "@bjoernboss/mws";

const page = new build.HtmlPage({
	head: [
		build.Title('My Page'),
		build.Meta('viewport', 'width=device-width, initial-scale=1'),
		build.LoadStyle('/style.css'),
		build.LoadScript('/app.js', { defer: '' })
	],
	body: [
		build.Div({ id: 'root' }, [
			build.Text('Hello, World!')
		])
	]
});

Plain strings passed as HtmlString values are automatically HTML-escaped. Use build.Safe(content) to mark trusted content that should not be escaped.

Internal Methods

Methods prefixed with _ (e.g. _rootAttachToServer, _pushTranslation, _restoreSnapshot) are framework-internal and must not be called by module implementations. They are public for cross-class access within the framework, but are not part of the public API and may change without notice.