Skip to content

serve — HTTP Server API ​

A pure-JS HTTP/1.1 server exposed as the global serve(). All protocol semantics — request parsing, routing, WebSocket upgrade, response serialization — run in JavaScript.

Global ​

GlobalTypeDescription
servefunctionStarts an HTTP server. Only one may be active at a time.

serve(options, handler) ​

Starts a listening server and returns a server handle. handler is called once per HTTP request; its return value (or resolved Promise value) is sent as the response.

js
let server = serve({ port: 8080 }, (req) => {
  if (req.pathname === '/hello') return 'Hello, world!';
  return { status: 404, headers: { 'Content-Type': 'text/plain' }, _body: 'Not found' };
});

Options ​

OptionDefaultDescription
port8080TCP port to listen on (0–65535).
hostname'127.0.0.1'Address to bind. Loopback only by default — pass '0.0.0.0' explicitly to accept connections from other hosts.
idleTimeout30000ms of connection inactivity before close; 0 disables.
ws{}Route table for WebSocket upgrades, keyed by request path.
tlsundefined{ cert, key } PEM strings to enable HTTPS (requires QZ_WITH_TLS).

Request object ​

handler receives a plain object describing the request:

FieldTypeDescription
methodstringHTTP method, e.g. 'GET'.
urlstringRaw request path including query, e.g. '/a?x=1'.
pathnamestringPath without query, e.g. '/a'.
searchstringQuery string including ?, or ''.
headersobjectLower-cased header names → values.
bodyReadableStream | nullRequest body when Content-Length > 0, otherwise null.
keepAlivebooleanWhether the connection stays open after this response.

The object also exposes async body readers:

js
serve({ port: 8080 }, async (req) => {
  let text = await req.text();          // full body as string
  let buf  = await req.arrayBuffer();   // full body as ArrayBuffer
  return { status: 200, headers: { 'Content-Type': 'text/plain' }, _body: text };
});

Bodies are fed straight from raw socket bytes into the ReadableStream, so binary request bodies are preserved.

Response values ​

handler may return (or resolve to):

  • A string — sent as 200 OK with Content-Type: text/plain; charset=utf-8.
  • An object with status, statusText, headers (a Headers instance or plain object), and _body (string, ArrayBuffer, or Uint8Array). Binary bodies go out as raw bytes.
  • null/undefined — 500 Internal Server Error (also used when the handler throws).
js
serve({ port: 8080 }, (req) => {
  if (req.method !== 'POST') return { status: 405, _body: 'POST only' };
  let data = new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF]);
  return {
    status: 201,
    headers: { 'Content-Type': 'application/octet-stream' },
    _body: data
  };
});

Server handle ​

serve() returns { closed, close() }:

js
let server = serve({ port: 8080 }, handler);
server.close();        // stops listening, releases the port

Only one server may run at a time. Calling serve() again while another is active throws serve: a server is already running (call srv.close() first).

WebSocket routes ​

options.ws maps paths to upgrade handlers. Each handler receives a connection object with onopen, onmessage, onclose, onerror, send(), and close().

js
serve({
  port: 8080,
  ws: {
    '/chat': (conn) => {
      conn.onmessage = (ev) => {
        conn.send('echo: ' + ev.data);       // text messages arrive as strings
      };
      conn.onclose = () => console.log('disconnected');
    }
  }
}, (req) => 'HTTP fallback');

A route value may also be an object with handler and an optional protocols array for subprotocol negotiation:

js
ws: {
  '/chat': {
    handler: (conn) => { conn.onmessage = (ev) => conn.send('pong'); },
    protocols: ['chat.v1']        // echoed via Sec-WebSocket-Protocol if offered
  }
}

Connection object:

MemberTypeDescription
send(data)functionSend a text message (string) or binary (Uint8Array).
close(code, reason)functionSend a close frame and mark the connection closed.
onopencallbackFires when the socket is ready.
onmessagecallbackReceives { data } — string for text frames, Uint8Array for binary.
onclosecallbackReceives { code, reason, wasClean }.
onerrorcallbackConnection error.

permessage-deflate (RFC 7692) compression is negotiated automatically when the client offers it and the native streaming deflate primitives are available (see compress).

The WebSocket handshake requires QZ_WITH_TEXTCODEC=ON and QZ_WITH_CRYPTO_EXT=ON at build time (SHA-1 accept key via crypto.subtle); otherwise upgrades throw WebSocket accept unavailable.

Requests without a matching ws route get a 404; an upgrade missing Sec-WebSocket-Key gets a 400.

TLS ​

Passing tls: { cert, key } enables HTTPS on the listener. Requires QZ_WITH_TLS=ON at build time (see Build Options).

js
serve({
  port: 8443,
  tls: { cert: pemCert, key: pemKey }
}, (req) => 'secure!');

gRPC Server ​

Passing grpc: server registers a gRPC service on the same listener. The gRPC stack is a pure-JS h2/HPACK/protobuf implementation. It lives on the same TCP port as the HTTP/1.1 handler — the listener dispatches on ALPN (h2 for TLS connections) or the PRI * HTTP/2.0 connection preface (h2c, plaintext).

js
const server = grpc.createServer();
server.addService(reg, { Echo: (call) => ({ text: call.request.text }) });
serve({ port: 50051, grpc: server }, () => 'not-grpc');

Requires QZ_WITH_GRPC=ON at build time (the gRPC bundle is opt-in because it adds ~3.5k lines to the polyfill; see Build Options).

For a complete unary/streaming walkthrough, see the grpc-hello example and the gRPC API reference.

Notes ​

  • HTTP/1.1 keep-alive is supported; the connection closes after the response when the client requests close or uses HTTP/1.0.
  • idleTimeout resets on each received byte and on each keep-alive response.
  • Headers parsed from the wire are lower-cased; response header names are passed through as given.
  • The server is synchronous in the sense that each handler runs to completion (or awaits); a slow handler blocks that connection only.

MIT Licensed