CLI recipes

Local first, public only when deliberate.

shell
# Local static build
node-http-server --root ./dist --port 8080

# Free local port; read the printed URL
node-http-server ./dist --port 0

# SPA plus eligible Brotli/gzip responses
node-http-server --root ./dist --spa --compression

# Deliberate /.well-known content; every dotfile becomes eligible for static serving
node-http-server --root ./public --allow-dotfiles

# LAN-facing — review firewall/auth/TLS first
node-http-server --root ./dist --host 0.0.0.0 --port 8080
Expected startup
node-http-server listening at http://127.0.0.1:8080
Failure checks: the root must already exist. Use the machine's real network address when connecting from another device.

Single-page application

Fallback only for application routes.

Node.js ESM
import {Server} from 'node-http-server';

new Server({
    root:'./dist',
    server:{
        spaFallback:true,
        compression:true,
        compressionThreshold:1024
    }
}).deploy();

Expected: an extensionless missing route accepting HTML falls back to index.html. Missing assets and requests not accepting HTML remain 404. Brotli/gzip is negotiated only for accepted, compressible, non-range responses at or above 1024 bytes.

Custom entry: use spaFallback:'app.html' or CLI --spa=app.html.

Ingress limits

Cap bodies and tighten request timing.

CommonJS
const {Server}=require('node-http-server');

new Server({
    root:'./public',
    server:{
        maxRequestBodyBytes:1024*1024,
        headersTimeout:15000,
        requestTimeout:30000,
        timeout:30000,
        keepAliveTimeout:5000
    }
}).deploy();

Expected: bodies above 1 MiB receive 413. Values are milliseconds except the body limit, which is bytes. False/0 disables a control; use disabled protection only intentionally.

Multiple servers and shutdown

Keep instances isolated and close both.

Node.js ESM
import {Server} from 'node-http-server';

const publicServer=new Server({
    root:'./public',
    port:8080
}).deploy();

const previewServer=new Server({
    root:'./preview',
    port:8081
}).deploy();

process.once('SIGTERM',async()=>{
    await Promise.all([
        publicServer.close(),
        previewServer.close()
    ]);
});

Expected: each instance owns its own root, port, configuration, and listener. close() resolves after owned listeners close; either instance can deploy again later.

Failure: calling deploy() again before close() completes throws. Port conflicts surface through the listener's native error event.

HTTPS only

Load fresh local certificate paths.

Node.js ESM
import {Server} from 'node-http-server';

new Server({
    host:'127.0.0.1',
    https:{
        privateKey:'./local-certs/private/server.key',
        certificate:'./local-certs/server.pub',
        port:8443,
        only:true
    }
}).deploy();

Expected: only secureServer listens on https://127.0.0.1:8443, negotiating HTTP/2 with HTTP/1.1 fallback on that port. Set only:false to add a separate plain HTTP listener; close() closes both. The HTTPS guide includes a complete paired-listener example.

Failure: key and certificate are both required. Generate fresh local material; old v8 fixture keys are public and untrusted.

HTTP/2 and HTTP/1.1

Compare both protocols on one HTTPS port.

Use Node.js 22.12.0 or newer and the repository's existing HTTPS echo server with the HTTP/2 client. These files live in the repository, outside the npm package. The client uses built-in node:http2 and node:https.

Run these commands from the repository root. Following the local certificate setup, select the existing v3_req extension so the certificate covers localhost and 127.0.0.1:

Local certificate setup
openssl genrsa -out local-certs/private/server.key 2048
openssl req -new -x509 -key local-certs/private/server.key -out local-certs/server.pub -days 30 -config local-certs/private/openssl.cnf -extensions v3_req -subj /CN=localhost
Terminal 1 — server
node example/proxy/https-and-http-basic.js

After the server's listeners are ready, run the client in a second terminal:

Terminal 2 — client
node example/advanced/https-http2-client.js

The default origin is https://127.0.0.1:4433. The client reads local-certs/server.pub once and supplies it through Node's native ca option. To use another HTTPS origin and certificate authority file, pass them in that order:

Optional client arguments
node example/advanced/https-http2-client.js https://127.0.0.1:4433 ./local-certs/server.pub
  • Expected HTTP/2: three concurrent requests to /one?request=1, /two?request=2, and /three?request=3 share one HTTP/2 session. Each response record includes its own stream ID, negotiated protocol, status, and complete body. Completion order may vary.
  • Expected HTTP/1.1: an independent request to /one?request=http1 uses the same HTTPS port and prints its observed protocol, status, and complete body.
  • Hook output: each echoed JSON body preserves the request URI and headers and adds httpVersion and httpVersionMajor. HTTP/2 bodies also include streamId, and their headers include pseudo-headers such as :path and :authority.
  • Plain HTTP: the separate listener uses port 8000. https.only:true removes this listener while retaining HTTP/1.1 fallback on the HTTPS port.

The server hook uses the same public serve(request,response,body) function for both protocols. HTTP/1.1 supplies native IncomingMessage/ServerResponse objects; HTTP/2 supplies Http2ServerRequest/Http2ServerResponse. Use shared response methods such as setHeader() and branch on request.httpVersionMajor===2 before reading request.stream. HTTP/2 responses cannot use HTTP/1.1 connection-specific headers such as Connection or Transfer-Encoding. See Node's compatibility API for native differences.

Choose HTTP/1.1-only HTTPS

Replace the server example's current http2 assignment with the following before its existing deploy() call, then restart the server:

Server configuration variant
config.https.http2 = false;

With HTTP/2 disabled, the client reports HTTP/2 connection/request failures while its independent HTTP/1.1 request can still complete. Restore http2:true or remove the override to negotiate both protocols again.

The client closes its HTTP/2 session after its requests settle. Ctrl+C stops the server through its signal handler, which awaits server.close() for the owned listeners and HTTP/2 sessions. Active requests may finish before shutdown completes. Use the existing multiple-server shutdown pattern when more than one instance is owned.

Host-based roots

Map accepted hostnames to directories.

CommonJS
const {Server}=require('node-http-server');

new Server({
    root:'./www/default',
    domain:'example.test',
    domains:{
        'docs.example.test':'./www/docs',
        'app.example.test':'./www/app'
    }
}).deploy();

Expected: matching Host values select their roots; unknown values receive 421. Hostname matching ignores port text in the Host header.

Separate concerns: configure DNS in the zone and use host for the bind address.

JSON route plus static files

Handle one route, then return to fallback.

Node.js ESM
import {Server} from 'node-http-server';

class AppServer extends Server{
    async onRequest(request,response,serve){
        if(request.url!='/health') return false;

        response.setHeader('Content-Type','application/json');
        await serve(request,response,JSON.stringify({ok:true}));
        return true;
    }
}

new AppServer({root:'./public'}).deploy();
Expected GET /health body
{"ok":true}

All other requests continue to static fallback. Return truthy only after the hook takes over. See the hook contract before asynchronous or buffered customization.

For text transformations in beforeServe, the advanced template, CSS, and request-timing examples decode the static Buffer with body.value.toString('utf8') before replacing text and set encoding.value='utf8' for the resulting string. Binary responses do not need text decoding.

Request logging

Append redacted NDJSON.

Node.js ESM
import {Server} from 'node-http-server';

new Server({
    root:'./public',
    log:'./logs/requests.ndjson',
    logBody:false,
    verbose:true
}).deploy();

Expected: one parseable JSON record is appended for each request. Common credential headers are redacted; body storage remains off.

Failure: create the parent directory first. The server continues after asynchronous write failures; verbose output exposes them and lastError retains the captured failure.

MIME and extension policy

Add one type and deny secrets.

Node.js ESM
import {Server} from 'node-http-server';

new Server({
    contentType:{
        md:'text/markdown; charset=utf-8',
        private:false
    },
    restrictedType:{
        key:true,
        pem:true
    }
}).deploy();

Expected: .md gets the custom type, .private returns 415, and .key/.pem return 403. Unknown extensions use application/octet-stream.