HTTP server utilities for JavaScript runtimes.
What is @rcompat/http?#
A cross-runtime HTTP server module using WHATWG Request and Response APIs.
Think of it as the server-side equivalent of fetch. Supports WebSockets and
SSL. Works consistently across Node, Deno, and Bun.
Installation#
npm install @rcompat/httppnpm add @rcompat/httpyarn add @rcompat/httpbun add @rcompat/httpUsage#
Basic server#
import http from "@rcompat/http";
const server = await http.serve(request => {
return new Response("Hello, world!");
}, { host: "localhost", port: 3000 });
// stop the server
server.stop();Routing#
import http from "@rcompat/http";
await http.serve(request => {
const { pathname } = new URL(request.url);
if (pathname === "/") {
return new Response("Home");
}
if (pathname === "/api/users") {
return Response.json([{ id: 1, name: "John" }]);
}
return new Response("Not Found", { status: 404 });
}, { host: "localhost", port: 3000 });HTTP methods#
import http from "@rcompat/http";
await http.serve(async request => {
const { pathname } = new URL(request.url);
const { method } = request;
if (pathname === "/api/data") {
if (method === "GET") {
return Response.json({ data: "value" });
}
if (method === "POST") {
const body = await request.json();
return Response.json({ received: body }, { status: 201 });
}
return new Response("Method Not Allowed", { status: 405 });
}
return new Response("Not Found", { status: 404 });
}, { host: "localhost", port: 3000 });Status codes#
import http from "@rcompat/http";
const { Status } = http;
await http.serve(request => {
return new Response("Created", { status: Status.CREATED });
}, { host: "localhost", port: 3000 });
// available status codes
Status.OK; // 200
Status.CREATED; // 201
Status.NO_CONTENT; // 204
Status.MOVED_PERMANENTLY; // 301
Status.NOT_MODIFIED; // 304
Status.BAD_REQUEST; // 400
Status.UNAUTHORIZED; // 401
Status.FORBIDDEN; // 403
Status.NOT_FOUND; // 404
Status.INTERNAL_SERVER_ERROR; // 500
// ... and many moreWebSockets#
import http from "@rcompat/http";
const server = await http.serve(request => {
// check if it's a WebSocket upgrade request
if (request.headers.get("upgrade") === "websocket") {
return server.upgrade(request, {
open(socket) {
console.log("Client connected");
socket.send("Welcome!");
},
message(socket, message) {
console.log("Received:", message);
socket.send(`Echo: ${message}`);
},
close(socket) {
console.log("Client disconnected");
},
});
}
return new Response("Hello!");
}, { host: "localhost", port: 3000 });SSL/HTTPS#
import http from "@rcompat/http";
import fs from "@rcompat/fs";
await http.serve(request => {
return new Response("Secure!");
}, {
host: "localhost",
port: 443,
ssl: {
cert: fs.ref("./certs/cert.pem"),
key: fs.ref("./certs/key.pem"),
},
});MIME types#
Get MIME types by extension or use predefined constants.
import http from "@rcompat/http";
http.toMIME("index.html"); // "text/html"
http.toMIME("app.js"); // "text/javascript"
http.toMIME("data.json"); // "application/json"
http.toMIME("image.png"); // "image/png"
http.MIME.APPLICATION_JSON; // "application/json"
http.MIME.APPLICATION_X_WWW_FORM_URLENCODED; // "application/x-www-form-urlencoded"
http.MIME.TEXT_EVENT_STREAM; // "text/event-stream"
http.MIME.TEXT_HTML; // "text/html"Serving static files#
import http from "@rcompat/http";
import fs from "@rcompat/fs";
const publicFiles = fs.ref("./public");
const files = new Map([
["/", publicFiles.join("index.html")],
["/app.js", publicFiles.join("app.js")],
]);
await http.serve(async request => {
const { pathname } = new URL(request.url);
const file = files.get(pathname);
if (file !== undefined && await file.exists()) {
return new Response(file.stream(), {
headers: { "content-type": http.toMIME(file.name) },
});
}
return new Response("Not Found", { status: 404 });
}, { host: "localhost", port: 3000 });API Reference#
Default export#
The default export groups HTTP constants and helpers:
| Property | Description |
|---|---|
MIME |
Uppercase MIME type constants |
Status |
HTTP status code constants |
methods |
Supported lowercase HTTP method names |
serve(handler, config) |
Start a runtime-native HTTP server |
toMIME(filename) |
Resolve a filename to a MIME type |
http.serve(handler, config)#
http.serve: (
handler: (request: Request) => Response | null | Promise<Response | null>,
config: Conf,
) => Promise<Server>;Creates an HTTP server.
| Parameter | Type | Description |
|---|---|---|
handler |
(request: Request) => Response |
Request handler function |
config |
Conf |
Server configuration |
Returns: A promise that resolves to a Server instance.
Conf#
interface Conf {
host: string;
port: number;
ssl?: {
cert: FileRef; // Path to certificate file
key: FileRef; // Path to key file
};
timeout?: {
inactivity?: number;
request?: number;
headers?: number;
};
}Timeout members are whole milliseconds:
inactivityis the maximum consecutive network inactivity during an in-flight request or response. Traffic resets it. It is not the keep-alive timeout after an exchange.requestis the maximum total time to receive the complete incoming request, including headers and body. Traffic does not reset it. Handler and response time are excluded.headersis the maximum total time to receive complete request headers.
| Member | Node | Bun | Deno |
|---|---|---|---|
inactivity |
Supported | Supported; rounded up to seconds | Unsupported |
request |
Supported | Unsupported | Unsupported |
headers |
Supported | Unsupported | Unsupported |
Omitting timeout, or using an empty object, preserves the runtime defaults.
Individual omitted members also preserve native defaults. Bun's current native
inactivity default is 10 seconds. Node currently defaults to no inactivity
timeout, a 300-second request timeout, and the lesser of 60 seconds or its
request timeout for headers. In particular, Node derives an omitted header
timeout as 0 when request: 0 is configured.
0 disables a supported clock. Configuring any timeout member on Deno, or
configuring request or headers on Bun, throws before the server binds—even
when the value is 0. Unsupported clocks are not silently ignored or emulated
as handler deadlines.
inactivity accepts values through 255000, Bun's portable maximum. Node-only
request and header clocks accept values through 2147483647. When both are
enabled, headers cannot exceed request; an explicit header timeout is also
validated against Node's effective request default.
The object is strict: unknown members and non-integer values are rejected. The previous numeric form is breaking and must be migrated:
// before
timeout: 30_000
// now
timeout: { inactivity: 30_000 }Server#
interface Server {
stop(): void;
upgrade(request: Request, actions: Actions): Response | null;
}| Method | Returns | Description |
|---|---|---|
stop() |
void |
Stop the server |
upgrade(request, actions) |
Response | null |
Upgrade to WebSocket |
Actions (WebSocket)#
interface Actions {
open?: (socket: WebSocket) => void;
message?: (socket: WebSocket, message: string | Uint8Array) => void;
close?: (socket: WebSocket, code: number, reason: string) => void;
}Text messages are strings and binary messages are Uint8Array values on every
runtime. The portable socket supports send(), close(), and readyState.
Node limits each assembled incoming or outgoing message to 16 MiB and does not
negotiate WebSocket extensions. When clients offer subprotocols, Node selects
the first offered protocol. Node caps queued outgoing data at 32 MiB;
bufferedAmount reports the bytes currently waiting to be written.
Status#
HTTP status code constants.
| Category | Codes |
|---|---|
| 1xx Info | CONTINUE, SWITCHING_PROTOCOLS, PROCESS, EARLY_HINTS |
| 2xx Success | OK, CREATED, ACCEPTED, NO_CONTENT, PARTIAL_CONTENT |
| 3xx Redirect | MOVED_PERMANENTLY, FOUND, SEE_OTHER, NOT_MODIFIED, TEMPORARY_REDIRECT, PERMANENT_REDIRECT |
| 4xx Client | BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, METHOD_NOT_ALLOWED, CONFLICT, GONE, UNPROCESSABLE_ENTITY, TOO_MANY_REQUESTS |
| 5xx Server | INTERNAL_SERVER_ERROR, NOT_IMPLEMENTED, BAD_GATEWAY, SERVICE_UNAVAILABLE, GATEWAY_TIMEOUT |
Examples#
REST API#
import http from "@rcompat/http";
const { Status } = http;
const users = new Map();
await http.serve(async request => {
const { pathname } = new URL(request.url);
const { method } = request;
// GET /users
if (method === "GET" && pathname === "/users") {
return Response.json([...users.values()]);
}
// POST /users
if (method === "POST" && pathname === "/users") {
const user = await request.json();
user.id = crypto.randomUUID();
users.set(user.id, user);
return Response.json(user, { status: Status.CREATED });
}
// GET /users/:id
const match = pathname.match(/^\/users\/(.+)$/);
if (method === "GET" && match) {
const user = users.get(match[1]);
if (user) return Response.json(user);
return Response.json({ error: "Not found" }, { status: Status.NOT_FOUND });
}
return new Response("Not Found", { status: Status.NOT_FOUND });
}, { host: "localhost", port: 3000 });Chat server with WebSockets#
import http from "@rcompat/http";
const clients = new Set();
const server = await http.serve(request => {
if (request.headers.get("upgrade") === "websocket") {
return server.upgrade(request, {
open(socket) {
clients.add(socket);
broadcast(`User joined (${clients.size} online)`);
},
message(socket, message) {
broadcast(`User: ${message}`);
},
close(socket) {
clients.delete(socket);
broadcast(`User left (${clients.size} online)`);
},
});
}
return new Response("WebSocket server");
}, { host: "localhost", port: 3000 });
function broadcast(message) {
for (const client of clients) {
client.send(message);
}
}Cross-Runtime Compatibility#
| Runtime | Supported |
|---|---|
| Node.js | ✓ |
| Deno | ✓ |
| Bun | ✓ |
No configuration required — just import and use.
License#
MIT
Contributing#
See CONTRIBUTING.md in the repository root.