Browse packages

Package · v0.33.0

http

Standard library HTTP

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#

bash
npm install @rcompat/http
bash
pnpm add @rcompat/http
bash
yarn add @rcompat/http
bash
bun add @rcompat/http

Usage#

Basic server#

js
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#

js
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#

js
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#

js
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 more

WebSockets#

js
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#

js
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.

js
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#

js
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)#

ts
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#

ts
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:

  • inactivity is 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.
  • request is 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.
  • headers is 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:

js
// before
timeout: 30_000

// now
timeout: { inactivity: 30_000 }

Server#

ts
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)#

ts
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#

js
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#

js
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.