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. 0.0.0.0 is a bind address, not a browser destination. Use the machine's real network address from another device.

Single-page application

Fallback only for application routes.

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.

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.

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.crt',
        port:8443,
        only:true
    }
}).deploy();

Expected: only secureServer listens on https://127.0.0.1:8443. Set only:false to run HTTP and HTTPS; close() closes both.

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

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. Port text in the Host header does not change hostname matching.

Separate concerns: this does not configure DNS or listen interfaces. Use host for the bind address.

JSON route plus static files

Handle one route, then return to fallback.

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.

Request logging

Append redacted NDJSON.

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. Async write failures do not stop the server; verbose output exposes them and lastError retains the captured failure.

MIME and extension policy

Add one type and deny secrets.

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.