Copyable tasks
Start from a working pattern.
Each example states what it should do and where to look when it does not.
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
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.
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.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.
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. Port text in the Host header does not change hostname matching.
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.
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.