http API
The http module starts servers. It has one function, http.serve, plus the response helpers you return from your handler.
Quick look
// server.js
http.serve({ port: 3000 }, (url, method, body) => {
return new Response("hello");
});
ff server.js
curl http://127.0.0.1:3000/
# hello
http.serve(signatures)
Three call shapes are accepted:
http.serve(handler); // default port 3000
http.serve(4242, handler); // port shorthand
http.serve({ port: 4242 }, handler); // options object
The server binds to 0.0.0.0. There is no host option.
Options reference
http.serve(
{
port: 3000, // default 3000
tls: { cert: "...", key: "..." }, // optional — enables HTTPS/WSS on the same port
websocket: { // optional — enables WebSocket upgrades
open: (socket) => {},
message: (socket, data) => {},
close: (socket) => {},
},
},
handler,
);
When the websocket block is present, any request carrying
Connection: Upgrade, Upgrade: websocket, and Sec-WebSocket-Key is
upgraded before your handler runs — the handler never sees that
request, and no { status: 101 } branch is needed (a handler that would
return 404 still gets the 101). The handler serves plain HTTP only:
// server.js
http.serve(
{
port: 3000,
websocket: {
open: (socket) => socket.send("welcome"),
message: (socket, data) => socket.send(data),
close: (socket) => console.log("bye"),
},
},
(url, method, body) => new Response("http here, ws upgrades on this port"),
);
Server sockets expose send / sendBinary only — track peers by object reference in a Set (see the WebSocket guide).
Handler reference
The handler is called as (url, method, body) — all three are strings:
Return one of:
Async handlers work — the 30s watchdog still applies:
http.serve({ port: 3000 }, async (url, method, body) => {
const res = await fetch("https://example.com/");
return new Response(await res.text());
});
Response helpers
Build responses with the global Response class:
return new Response("text"); // 200 text/plain by default
return new Response("missing", { status: 404 }); // custom status
return Response.json({ hello: "world" }); // JSON + content-type, optional init
return Response.json({ id: 1 }, { status: 201 }); // with status
return Response.redirect("/new-path"); // 302 status only — no Location header
return Response.redirect("/moved", 301); // 301 status only — no Location header
Read bodies back with text() / json() / arrayBuffer() / bytes() / blob() / formData() — all work on Request and Response. Each body reads once (bodyUsed goes true).
Headers are case-insensitive and lowercased internally:
const h = new Headers({ "Content-Type": "text/html" });
h.get("content-type"); // "text/html"
h.set("x-id", "1");
h.append("x-id", "2");
h.getAll("x-id"); // ["1", "2"]
h.has("x-id"); // true
h.delete("x-id");
h.entries(); h.keys(); h.values();
h.forEach((value, key) => console.log(key, value));
Full example
// server.js
http.serve({ port: 3000 }, (url, method, body) => {
const path = new URL(url, "http://localhost").pathname;
if (path === "/json") return Response.json({ ok: true });
if (path === "/old") return Response.redirect("/json", 301);
if (path === "/echo" && method === "POST") {
return new Response(body, {
headers: { "content-type": "text/plain" },
});
}
return new Response("not found", { status: 404 });
});
ff server.js
curl http://127.0.0.1:3000/json # {"ok":true}
curl -i http://127.0.0.1:3000/old # 301 without Location (clients won't follow)
curl -X POST http://127.0.0.1:3000/echo -d hi # hi
Limits (summary)
Full table lives in the Limitations reference; the ones that bite here: 512 concurrent connections, 10 MB max response body (the 64 KB staging buffer spills to the heap), 30s handler watchdog (→ 504), plain listeners speak HTTP/1.1 (HTTP/2 needs TLS).
See also