Copyable Node.js tasks
Start from a working pattern.
Each example is a Node.js program and states the expected result and its matching failure checks. Browser paths are content served by that program, not browser execution of node-http-server.
CLI recipes
Local first, public only when deliberate.
# 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 8080node-http-server listening at http://127.0.0.1:8080
Single-page application
Fallback only for application routes.
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.
spaFallback:'app.html' or CLI --spa=app.html.Ingress limits
Cap bodies and tighten request timing.
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.
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.
deploy() again before close() completes throws. Port conflicts surface through the listener's native error event.HTTPS only
Load fresh local certificate paths.
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.
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:
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
node example/proxy/https-and-http-basic.js
After the server's listeners are ready, run the client in a second terminal:
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:
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=3share 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=http1uses 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
httpVersionandhttpVersionMajor. HTTP/2 bodies also includestreamId, and their headers include pseudo-headers such as:pathand:authority. - Plain HTTP: the separate listener uses port
8000.https.only:trueremoves 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:
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.
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.
host for the bind address.JSON route plus static files
Handle one route, then return to fallback.
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();{"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.
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.
lastError retains the captured failure.MIME and extension policy
Add one type and deny secrets.
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.