
Security News
The Hidden Blast Radius of the Axios Compromise
The Axios compromise shows how time-dependent dependency resolution makes exposure harder to detect and contain.
@sourceregistry/node-webserver
Advanced tools
TypeScript web server for Node.js with web-standard Request and Response APIs
TypeScript web server for Node.js built around the web platform Request and Response APIs.
It provides:
pre() and post()npm install @sourceregistry/node-webserver
Node.js 18+ is required.
import { WebServer, json, text } from "@sourceregistry/node-webserver";
const app = new WebServer();
app.GET("/", () => text("hello world"));
app.GET("/health", () => json({
ok: true
}));
app.listen(3000, () => {
console.log("listening on http://127.0.0.1:3000");
});
import { WebServer } from "@sourceregistry/node-webserver";
const app = new WebServer();
WebServer extends Router, so you can register routes and middleware directly on app.
You can also pass handler callbacks for locals and platform:
const app = new WebServer({
locals: (event) => ({
requestId: crypto.randomUUID(),
ip: event.getClientAddress()
}),
platform: () => ({
name: "node"
})
});
app.GET("/users", async () => {
return new Response("all users");
});
app.GET("/users/[id]", async (event) => {
return new Response(`user ${event.params.id}`);
});
app.POST("/users", async (event) => {
const body = await event.request.json();
return json({ created: true, body }, { status: 201 });
});
Supported HTTP methods:
GETPOSTPUTPATCHDELETEHEADOPTIONSUSE to register the same handler for all methodsimport { Router } from "@sourceregistry/node-webserver";
const api = new Router();
api.GET("/status", () => new Response("ok"));
app.use("/api", api);
The library exports helpers for common content types:
import { html, json, text } from "@sourceregistry/node-webserver";
app.GET("/", () => html("<h1>Hello</h1>"));
app.GET("/message", () => text("plain text"));
app.GET("/data", () => json({ ok: true }));
It also exports redirect() and error() for control flow. These helpers throw a Response, and the router immediately returns that response without continuing route resolution. This works in normal routes, middleware, lifecycle hooks, and nested routers.
import { error, redirect } from "@sourceregistry/node-webserver";
app.GET("/old", () => {
redirect(302, "/new");
});
app.GET("/admin", (event) => {
if (!event.locals.userId) {
error(401, { message: "Unauthorized" });
}
return new Response("secret");
});
Nested routers short-circuit the same way:
const api = new Router();
api.GET("/legacy", () => {
redirect(301, "/api/v2");
});
app.use("/api", api);
It also exports sse() for Server-Sent Events. The helper creates a streaming response and passes your callback an emit() function. You can also pass a ResponseInit object to override status or headers.
import { sse } from "@sourceregistry/node-webserver";
app.GET("/events", sse((event, emit) => {
emit({ connected: true }, { event: "ready", id: "1" });
emit(`hello ${event.getClientAddress()}`);
}, {
status: 200,
headers: {
"x-stream": "enabled"
}
}));
emit(data, options) supports:
event for the SSE event nameid for the SSE event idretry for the reconnection delaycomment for SSE comment linesObjects are serialized as JSON automatically. Strings are sent as plain data: lines.
If the callback finishes without returning cleanup, the SSE stream closes automatically. If it returns a cleanup function, the stream stays open until the client disconnects or the stream is canceled.
Route handlers receive a web-standard Request plus extra routing data:
app.GET("/posts/[slug]", async (event) => {
const userAgent = event.request.headers.get("user-agent");
const slug = event.params.slug;
const ip = event.getClientAddress();
event.setHeaders({
"Cache-Control": "no-store"
});
return json({
slug,
userAgent,
ip
});
});
Available fields include:
event.requestevent.urlevent.fetch(...)event.paramsevent.localsevent.platformevent.cookiesevent.getClientAddress()event.setHeaders(...)event.fetch(...) is a server-aware variant of the native Fetch API:
cookie and authorization headers by defaultapp.GET("/posts", async (event) => {
const response = await event.fetch("/api/posts");
return new Response(await response.text(), {
headers: {
"content-type": response.headers.get("content-type") ?? "text/plain"
}
});
});
You can extend the request-local and platform typings by adding your own app.d.ts file in your project:
declare global {
namespace App {
interface Locals {
userId?: string;
requestId: string;
}
interface Platform {
name: string;
}
}
}
export {};
The server will use those App.Locals and App.Platform definitions automatically in route handlers, middleware, and lifecycle hooks.
Middleware wraps request handling and can short-circuit the chain.
app.useMiddleware(async (event, next) => {
const startedAt = Date.now();
const response = await next();
if (!response) {
return new Response("No response", { status: 500 });
}
const nextResponse = new Response(response.body, response);
nextResponse.headers.set("x-response-time", String(Date.now() - startedAt));
return nextResponse;
});
The library exports built-in middleware namespaces from @sourceregistry/node-webserver:
CORSRateLimiterRequestIdSecurityTimeoutUse Security.headers() to apply a small set of secure defaults without overwriting headers your route already sets.
import { Security } from "@sourceregistry/node-webserver";
app.useMiddleware(Security.headers());
By default it adds:
Content-Security-PolicyX-Frame-OptionsReferrer-PolicyPermissions-PolicyCross-Origin-Opener-PolicyCross-Origin-Resource-PolicyYou can disable or override individual headers:
app.useMiddleware(Security.headers({
contentSecurityPolicy: false,
frameOptions: "SAMEORIGIN",
strictTransportSecurity: "max-age=31536000; includeSubDomains"
}));
Use RequestId.assign() to accept or generate a request ID, expose it through event.locals.requestId, and add it to the response.
import { RequestId } from "@sourceregistry/node-webserver";
app.useMiddleware(RequestId.assign());
You can customize the header name or generator:
app.useMiddleware(RequestId.assign({
headerName: "x-correlation-id",
generate: () => crypto.randomUUID()
}));
Use Timeout.deadline() to return a fallback response when a route takes too long.
import { Timeout } from "@sourceregistry/node-webserver";
app.useMiddleware(Timeout.deadline({
ms: 5000
}));
You can customize the response and add a timeout hook:
app.useMiddleware(Timeout.deadline({
ms: 2000,
status: 503,
body: "Request timed out",
onTimeout: () => {
console.warn("request exceeded deadline");
}
}));
Route-specific middleware:
const requireApiKey = async (event, next) => {
if (event.request.headers.get("x-api-key") !== process.env.API_KEY) {
return new Response("Unauthorized", { status: 401 });
}
return next();
};
app.GET("/admin", () => new Response("secret"), requireApiKey);
Use enhance() when you want to derive typed request-scoped data for a single handler without putting everything on event.locals.
Each enhancer receives the normal request event and can:
event.contextundefined to contribute nothingResponse to short-circuit the route earlyerror(...), redirect(...), or new Response(...) for the same control flow used elsewhere in the routerimport { enhance, error } from "@sourceregistry/node-webserver";
app.GET("/admin", enhance(
async (event) => {
return new Response(JSON.stringify({
userId: event.context.user.id,
requestId: event.context.requestId
}), {
headers: {
"content-type": "application/json"
}
});
},
async (event) => {
const token = event.request.headers.get("authorization");
if (!token) {
error(401, { message: "Unauthorized" });
}
return {
user: { id: "u_1", role: "admin" }
};
},
async (event) => {
return {
requestId: event.locals.requestId
};
}
));
Use pre() for logic that should run before route resolution, and post() for logic that should run after a response has been produced.
pre()pre() can short-circuit the request by returning a Response.
app.pre(async (event) => {
if (!event.request.headers.get("authorization")) {
return new Response("Unauthorized", { status: 401 });
}
});
post()post() receives the final response and may replace it.
app.post(async (_event, response) => {
const nextResponse = new Response(response.body, response);
nextResponse.headers.set("x-powered-by", "node-webserver");
return nextResponse;
});
app.GET("/login", async (event) => {
event.cookies.set("session", "abc123", {
path: "/",
httpOnly: true,
sameSite: "lax",
secure: true
});
return new Response("logged in");
});
app.GET("/me", async (event) => {
const session = event.cookies.get("session");
return json({ session });
});
app.POST("/logout", async (event) => {
event.cookies.delete("session", {
path: "/",
httpOnly: true,
secure: true
});
return new Response("logged out");
});
app.WS("/ws/chat/[room]", async (event) => {
const room = event.params.room;
const ws = event.websocket;
ws.send(`joined:${room}`);
ws.on("message", (message) => {
ws.send(`echo:${message.toString()}`);
});
});
Use dir() to expose a directory through a route, or serveStatic() directly if you want manual control.
import { dir } from "@sourceregistry/node-webserver";
app.GET("/assets/[...path]", dir("./public/assets"));
app.GET("/", dir("./public"));
Manual usage:
import { serveStatic } from "@sourceregistry/node-webserver";
app.GET("/downloads/[...path]", (event) => {
return serveStatic("./downloads", event, {
cacheControl: "public, max-age=3600"
});
});
The helper canonicalizes and validates the requested path, rejects traversal attempts such as ../secret.txt and encoded variants like ..%2fsecret.txt, and verifies that symlinks cannot escape the configured root.
The server includes a security config block for safer defaults.
const app = new WebServer({
type: "http",
options: {},
security: {
maxRequestBodySize: 1024 * 1024,
headersTimeoutMs: 30_000,
requestTimeoutMs: 60_000,
keepAliveTimeoutMs: 5_000,
maxWebSocketPayload: 64 * 1024,
trustedProxies: ["127.0.0.1"],
trustHostHeader: true,
allowedWebSocketOrigins: [
"https://app.example.com",
"https://admin.example.com"
]
}
});
Available options:
trustHostHeaderallowedHoststrustedProxiesallowedWebSocketOriginsmaxRequestBodySizeheadersTimeoutMsrequestTimeoutMskeepAliveTimeoutMsmaxWebSocketPayloadtrustHostHeader defaults to false. That is the safer default for public-facing services unless you are explicitly validating proxy behavior.
trustedProxies is also disabled by default. When configured, the server will trust X-Forwarded-For, X-Forwarded-Proto, and X-Forwarded-Host only when the direct peer matches one of the configured values.
For public-facing services, the server now also applies conservative timeout defaults unless you override them:
headersTimeoutMs: 30000requestTimeoutMs: 60000keepAliveTimeoutMs: 5000import { CORS } from "@sourceregistry/node-webserver";
app.useMiddleware(CORS.policy({
origin: ["https://app.example.com"],
credentials: true,
methods: ["GET", "POST", "DELETE"]
}));
The library provides two rate limiter algorithms:
import { RateLimiter } from "@sourceregistry/node-webserver";
app.useMiddleware(RateLimiter.fixedWindowLimit({
windowMs: 60_000,
max: 100
}));
More accurate than fixed window as it tracks individual request timestamps. Uses more memory but avoids the "cliff" effect where all requests at the end of one window and start of next are counted together.
import { RateLimiter } from "@sourceregistry/node-webserver";
app.useMiddleware(RateLimiter.slidingWindowLimit({
windowMs: 60_000,
max: 100
}));
Both limiters support the same options:
windowMs: Window duration in milliseconds (default: 60_000)max: Maximum requests per windowkey: Function to generate key (default: IP address)message: Custom error messagestatusCode: HTTP status code (default: 429)headers: Include or remove rate limit headers (default: 'include')onRateLimit: Callback when rate limit is hitstore: Custom storage backendThe onRateLimit callback receives additional metadata:
RateLimiter.fixedWindowLimit({
max: 300,
windowMs: 60_000,
onRateLimit: (event, info) => {
event.setHeaders({
"X-RateLimit-Limit": info.max.toString(),
"X-RateLimit-Remaining": info.remaining.toString(),
"X-RateLimit-Reset": info.reset.toString(),
"Retry-After": Math.ceil(info.resetTimeMs / 1000).toString()
});
}
});
import { readFileSync } from "node:fs";
import { WebServer } from "@sourceregistry/node-webserver";
const app = new WebServer({
type: "https",
options: {
key: readFileSync("./certs/server.key"),
cert: readFileSync("./certs/server.crt")
}
});
app.GET("/", () => new Response("secure"));
app.listen(3443);
For a production-oriented baseline with:
see examples/public-baseline.ts.
npm test
npm run build
Apache-2.0. See LICENSE.
FAQs
TypeScript web server for Node.js with web-standard Request and Response APIs
We found that @sourceregistry/node-webserver demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
The Axios compromise shows how time-dependent dependency resolution makes exposure harder to detect and contain.

Research
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.

Research
Malicious versions of the Telnyx Python SDK on PyPI delivered credential-stealing malware via a multi-stage supply chain attack.