node-http-server

September 7, 2026 · View on GitHub

node-http-server — HTTP and HTTPS static server for Node.js

node-http-server

HTTP and HTTPS static serving for Node.js — CLI, CommonJS, ESM, and zero runtime dependencies.

CI npm version npm downloads license supported Node.js version runtime dependencies protocols: HTTP + HTTPS line coverage function coverage branch coverage

Start · Why · Docs hub · CLI · Library API · Configuration · HTTPS · Examples · Playground · Testing · Performance · Benchmarks · Operations

Sponsor RIAEvangelist to help development of node-http-server

node-http-server serves static files over HTTP and HTTPS. HTTPS defaults to HTTP/2 through Node's built-in node:http2, with HTTP/1.1 negotiated on the same port for clients that need it, and adds zero runtime dependencies. The CLI starts HTTP/1.1; CommonJS and ESM support HTTP, HTTPS-only, and paired HTTP + HTTPS listeners. The sole direct development dependency is the owner-maintained vanilla-test@2.1.1, used for project-owned native V8 coverage.

Runtime boundary: node-http-server is Node.js-only; native-browser execution, import maps, and browser-bundler conformance are not applicable. It serves files to browsers; the package itself does not execute in browsers. CommonJS and ESM are Node.js module entry points, not browser entry points. A bundler is supported only when it targets Node.js; browser-targeted bundles and file:// are not supported runtime paths. The documentation site and configuration playground run in a browser only to display or generate Node.js examples—they never import or execute this package. Because the package has zero runtime dependencies, consumer-root dependency-conflict and scoped import-map tests are also not applicable.

Version 10 adds HTTP/2 to the static-server toolkit with streaming files, clean multi-server lifecycle, modern cache and range behavior, optional compression and SPA fallback, configurable request limits, and strict root containment.

HTTP and HTTPS modes

ModeStart it withActive listener
HTTPCLI or module API with the default configurationserver.server
HTTPS onlyCommonJS or ESM with key/certificate paths and https.only:trueserver.secureServer
HTTP + HTTPSCommonJS or ESM with key/certificate paths and https.only:falseserver.server and server.secureServer

Both transports use the same roots, hooks, request-body handling, range behavior, cache validation, and static-file pipeline. HTTPS selects HTTP/2 or HTTP/1.1 during the TLS handshake using ALPN; no failed HTTP/2 request or retry is needed. Plain HTTP remains HTTP/1.1. See the focused HTTPS guide for complete CommonJS and ESM examples and protocol-specific timeouts.

Why node-http-server

SignalEngineering value
Zero runtime dependenciesA compact install and an inspectable runtime surface.
CLI + CommonJS + ESMOne server fits shell tasks, existing Node applications, and modern modules.
Native HTTP + HTTPSNode's built-in node:http, node:http2, and node:https, with negotiated HTTP/2 over HTTPS and HTTP/1.1 compatibility.
Modern static deliveryStreaming, HEAD, ranges, validators, Brotli/gzip, SPA fallback, and MIME controls across both transports.
Explicit security and operationsLocalhost binding, root containment, dotfile policy, Host routing, HTTPS, limits, timeouts, and logs.
Measured deliveryFocused protocol tests, per-file native V8 coverage, packed-package smoke checks, and reproducible benchmarks.

See the compact decision guide for project fit and source links.

9.1.0 measured core results

Nine alternating samples on Node 24.18.0 compare the exact 9.0.2 tag with 9.1.0. Every timed response is validated, and the complete samples, environment, configuration, and source hashes are published in the raw result.

Targeted path9.0.2 median9.1.0 medianResult
1,000 repeated query values1,076 req/s7,695 req/s7.15× speedup
1,000-domain routing miss13,926 req/s23,259 req/s1.67× speedup
16-byte custom-hook range from 8 MiB790 req/s2,310 req/s2.92× speedup
Default Brotli for 2.5 MiB structured text0.580 req/s112.267 req/s193.56× speedup; 161,802 → 270,759 compressed bytes
Cold Config construction201,737 instances/s580,412 instances/s2.88× speedup
Cold Config retained bytes11,816.5 B/instance5,786.0 B/instance51.0% lower
Immediate deploy + close180 leaked listeners0 leaked listenersClean closure

The Performance page includes representative paths, compatibility controls, workload contracts, and reproduction commands.

Install

npm install node-http-server

Node.js 22.12 or newer is required.

Start a server

CommonJS

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

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

server.deploy();

ESM

import {Server} from 'node-http-server';

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

server.deploy();

Both module systems also expose the original default singleton:

// CommonJS
const server=require('node-http-server');
server.deploy({root:'./public'});
// ESM
import server from 'node-http-server';
server.deploy({root:'./public'});

The default address is http://127.0.0.1:8080. Set host:'0.0.0.0' only when other machines should be able to connect.

Module styleDefault exportNamed exports
CommonJSrequire('node-http-server')Server, Config, RefString
ESMimport server from 'node-http-server'Server, Config, RefString

CLI

Install globally when you want the command everywhere:

npm install --global node-http-server
node-http-server --root ./public --port 8080

Or run the package directly through npm:

npx node-http-server --root ./public
OptionPurpose
-p, --port <port>HTTP port; default 8080
-r, --root <path>Static root; default current directory
--host <address>Listen address; default 127.0.0.1
--domain <hostname>Expected primary Host value
--index <file>Directory index; default index.html
--no-cacheSend no-cache response directives
--cacheAllow client caching
--allow-dotfilesAllow dot-prefixed path segments; blocked by default
--spa[=<file>]Enable SPA fallback; optional fallback file
--compressionEnable negotiated Brotli or gzip responses
--max-body <bytes or false>Set the request-body limit; false, off, or 0 is unlimited
--timeout <ms or false>Set the socket inactivity timeout; false, off, or 0 disables
--request-timeout <ms or false>Set the complete-request timeout; false, off, or 0 disables
--headers-timeout <ms or false>Set the request-header timeout; false, off, or 0 disables
--keep-alive-timeout <ms or false>Set the keep-alive timeout; false, off, or 0 disables
--log <path>Append request records as NDJSON
-v, --verbosePrint server activity
-h, --helpPrint command help
--versionPrint the package version

The v8 key=value form still works:

node-http-server root=./public port=9000 verbose=true

CLI examples

UseCommand
Serve the current directory locallynode-http-server
Serve another directorynode-http-server --root ./public
Use a different local portnode-http-server --port 9000
Accept LAN/network connectionsnode-http-server --host 0.0.0.0
Enable SPA fallback and compressionnode-http-server --spa --compression
Use a custom SPA entrynode-http-server --spa=app.html
Limit bodies to 1 MiBnode-http-server --max-body 1048576
Disable the request timeoutnode-http-server --request-timeout false
Disable socket inactivity timeoutnode-http-server --timeout false
Allow client cachingnode-http-server --cache
Deliberately serve /.well-knownnode-http-server --root ./public --allow-dotfiles
Write NDJSON request logsnode-http-server --log ./requests.ndjson

Use the module API for HTTPS certificates, virtual hosts, hooks, Brotli quality, compression thresholds, and custom configuration functions.

Server lifecycle

deploy(config?, readyCallback?) starts the configured HTTP listener and optional HTTPS listener, then returns the Server instance. The callback receives the instance and its ready Node listener. It runs once for each listener when both protocols are enabled.

close(callback?) closes every listener owned by the instance and returns a Promise. HTTP/2 sessions close gracefully after their active streams finish. The same instance can be deployed again after it closes.

import {Server} from 'node-http-server';

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

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

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

Each Server owns isolated configuration and listener state. The active Node listeners remain available as server.server and, when configured, server.secureServer.

Server API

MemberReturnsPurpose
deploy(config?, callback?)ServerStart the instance's HTTP and optional HTTPS listeners
close(callback?)Promise<void>Close every listener owned by the instance
address()address object or nullRead the first active listener address
serve(request, response, body?, encoding?)PromiseComplete a manual response through beforeServe
serveFile(filename, request, response)Promise<boolean>Serve a deliberate file from custom code
configConfigIsolated active configuration
serverNode HTTP server or nullActive HTTP listener
secureServerNode Http2SecureServer, https.Server, or nullActive HTTPS listener; https.http2:false selects https.Server
lastErrorerror or nullLast captured request, hook, stream, HTTP/2 session, or logging error

Node listener errors keep Node's native event contract. Attach an error handler after deploy() when the application needs to handle bind failures:

server.deploy();
server.server.once('error',error=>console.error(error));

Attach the same handler to server.secureServer when HTTPS also runs. serveFile() trusts its filename and is a deliberate escape hatch from automatic routing policies, including dotfile blocking; never pass unvalidated request input to it.

Configuration

Pass configuration to new Server(config) or server.deploy(config). Known nested objects merge with isolated defaults, so changing one instance never changes another. Unsafe prototype keys are rejected.

const config={
    host:'127.0.0.1',
    port:8080,
    root:'./public',
    verbose:false,
    server:{
        index:'index.html',
        noCache:false,
        allowDotfiles:false,
        maxRequestBodyBytes:1024*1024,
        compression:true,
        compressionThreshold:1024,
        brotliQuality:4,
        spaFallback:false
    }
};

Top-level values

KeyDefaultDescription
host'127.0.0.1'Address used by listen()
port8080HTTP port
rootprocess.cwd()Static file root
domain'0.0.0.0'Legacy primary Host check; host controls the listen address
domains{}Additional hostname-to-root mappings
verbosefalseConsole activity output
logfalseNDJSON log path, or false to disable request logging
logFunctionbuilt-in loggerFunction used when log is enabled
logBodyfalseInclude the UTF-8 request body in custom/default log records
contentTypebuilt-in mapMIME overrides/additions, or false to disable automatic mapping
restrictedType{}Extension keys that should return 403
errorsbuilt-in responsesError headers and bodies for 400, 403, 404, 405, 413, 415, 416, 421, and 500
httpsdisabledHTTPS certificate and listener configuration
servershown belowHTTP behavior and timeout settings

server values

KeyDefaultDescription
index'index.html'File used for directory requests
noCachetrueSend no-cache response directives
allowDotfilesfalseAllow any dot-prefixed path segment; only literal true opts in
timeout30000HTTP/1 socket or HTTP/2 session inactivity timeout in milliseconds
requestTimeout300000HTTP/1 complete-request timeout in milliseconds
headersTimeout60000HTTP/1 request-header timeout in milliseconds
keepAliveTimeout5000HTTP/1 keep-alive timeout in milliseconds
maxRequestBodyBytesfalseMaximum body size in bytes; false, null, or 0 means unlimited
compressionfalseNegotiate Brotli or gzip through Node's built-in node:zlib; runtime dependencies stay at zero
compressionThreshold1024Minimum uncompressed size in bytes
brotliQuality4Brotli quality from 0 through 11 for automatically compressed static responses through node:zlib
spaFallbackfalsetrue uses server.index; a string selects another fallback file

Every timeout accepts a nonnegative millisecond value. Programmatic configuration accepts false, null, or 0 to disable it. Limits and compression stay under your control; no request-body limit or compression is enabled by default.

requestTimeout, headersTimeout, and keepAliveTimeout apply to HTTP/1 connections, including those negotiated on the HTTPS listener. They do not impose HTTP/2 stream deadlines; timeout controls HTTP/2 session inactivity.

Disable and opt-out values

Settingfalsenull0
server.timeoutDisabledDisabledDisabled
server.requestTimeoutDisabledDisabledDisabled
server.headersTimeoutDisabledDisabledDisabled
server.keepAliveTimeoutDisabledDisabledDisabled
server.maxRequestBodyBytesUnlimitedUnlimitedUnlimited
server.allowDotfilesDotfiles blockedInvalidInvalid
contentTypeAutomatic MIME map removed; files use application/octet-streamUnsupported map valueUnsupported map value

Deployment validates port ranges, requires a nonempty listen address, verifies static roots, and rejects negative timeout or limit values before opening a listener.

Config

new Config(values?) creates an isolated configuration. config.merge(values) safely merges another set and returns the same instance. Config.defaults and Config.mimeTypes each return a fresh copy.

The configuration and MIME map also have explicit package subpaths:

// CommonJS
const Config=require('node-http-server/config');
const contentTypes=require('node-http-server/mime-types');
// ESM
import Config from 'node-http-server/config';
import contentTypes from 'node-http-server/mime-types';

Built-in modern MIME types

The built-in MIME map is isolated in the small server/MimeTypes.js file. A contentType object adds or overrides entries:

new Server({
    contentType:{
        md:'text/markdown; charset=utf-8'
    }
});

Unknown extensions use application/octet-stream. Set contentType:false to remove the map from the active configuration; static files then use the same safe binary fallback unless a hook sets another type. Set one extension to false inside the map when that extension should return 415 Unsupported Media Type.

HTTPS

HTTPS is a first-class module API mode built on Node's node:http2.createSecureServer() with allowHTTP1:true. Clients negotiate HTTP/2 or HTTP/1.1 on the same HTTPS port. It shares the HTTP request pipeline and keeps runtime dependencies at zero on Node.js 22.12 or newer. Use the focused HTTPS guide for HTTPS-only, paired-listener, lifecycle, and certificate examples.

KeyDefaultDescription
https.ca''Optional CA certificate path
https.privateKey''Private-key path
https.certificate''Certificate path
https.passphrasefalseOptional private-key passphrase
https.port443HTTPS port
https.onlyfalseSkip the HTTP listener when HTTPS is configured
https.http2trueEnable HTTP/2 with HTTP/1.1 negotiation; false selects the original node:https listener
const secureServer=new Server({
    host:'127.0.0.1',
    https:{
        privateKey:'/path/to/private.key',
        certificate:'/path/to/certificate.pem',
        ca:'/path/to/ca.pem',
        passphrase:false,
        port:8443,
        only:true
    }
});

secureServer.deploy();

Leave only:false to run HTTP and HTTPS together. close() closes both listeners and gracefully closes active HTTP/2 sessions. HTTPS still requires your configured key and certificate paths; HTTP/2 does not enable TLS for the plain HTTP listener.

Set https.http2:false when an application needs the original https.Server type or HTTP/1-specific native APIs. With the default, server.secureServer is a Node Http2SecureServer, and hooks receive Node's HTTP/2 compatibility request/response objects for HTTP/2 requests. Cleartext HTTP/2 (h2c), HTTP/2-only mode, and HTTP/3 are not provided.

Multiple domains

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

deploy() compiles domain, domains, and their canonical roots into an O(1) routing table. Assigning server.config.root or server.config.domain, or changing entries in server.config.domains, invalidates the table; the next request rebuilds it with the live values.

host decides which network interface listens. domain and domains decide which HTTP/1 Host values or HTTP/2 :authority values and roots the server accepts. A wildcard primary domain ('0.0.0.0' or '*') selects the primary root before the domains map; set a non-wildcard primary domain when using virtual hosts.

Error responses and extension controls

SettingShapePurpose
errors.headersheader objectHeaders added to built-in error responses
errors[status]stringBody for 400, 403, 404, 405, 413, 415, 416, 421, or 500
restrictedType{extension:true}Return 403 for selected extensions
contentType entry{extension:false}Return 415 for one selected extension
domains{hostname:root}Map accepted Host values to isolated static roots

Static HTTP and HTTPS behavior

  • GET streams files instead of loading every file into memory.
  • HEAD returns the same status and headers with an empty response body.
  • Other methods reach the hooks first, then receive 405 Method Not Allowed from the static fallback.
  • A satisfiable single GET byte range returns 206 Partial Content; a valid but unsatisfiable range returns 416.
  • Malformed, unsupported-unit, and multi-range headers are ignored, so the response remains a full 200. HEAD ignores Range and mirrors the full GET headers with an empty body.
  • Weak ETags and Last-Modified support conditional 304 Not Modified responses.
  • Automatic compression negotiates Brotli or gzip for eligible static responses when enabled, accepted by the client, above the threshold, and outside byte-range handling. Manual serve() responses retain their caller-selected encoding.
  • SPA fallback is off by default. When enabled, an extensionless missing path that accepts HTML falls back to the configured index or root-relative filename.
  • Requested paths are decoded and resolved inside the configured root. Traversal and filesystem escapes are rejected.
  • Dot-prefixed path segments return 403 before filesystem lookup or SPA fallback. Set server.allowDotfiles:true only when the entire root is safe to expose, including paths such as /.well-known.

See SECURITY.md before exposing a server outside the local machine.

Request data and limits

The parsed request passed to onRequest includes both body forms:

MemberTypeValue
request.bodystringUTF-8 request body
request.bodyBufferBufferOriginal request bytes
request.uriobjectParsed URL information and query
request.urlstringProcessed request path
request.serverRootstringSelected static root

When maxRequestBodyBytes is set and the request crosses it, the static lifecycle stops with 413 Payload Too Large.

HTTP/2 request bodies are read through the request stream even without Content-Length. The same complete body forms reach onRequest for both protocols.

Hooks

Subclass Server or assign hook functions to intercept the lifecycle. The first three hooks may return a value directly or through a Promise.

HookArgumentsWhen it runs
onRawRequestrequest, response, serveImmediately after receipt, before body parsing
onRequestrequest, response, serveAfter the request body and URL helpers are ready
beforeServerequest, response, bodyRef, encodingRef, serveImmediately before a buffered response is sent
afterServerequest, responseAfter a library completion path finishes

Return a truthy value from the first three hooks when the hook is taking over that step. Complete the response with the supplied serve function or the Node response object.

Hooks receive native Node request/response objects for the negotiated protocol. Use public methods such as response.setHeader() and response.end() and the supplied serve continuation. HTTP/2 hooks must use HTTP/2-compatible headers: connection-specific headers such as Connection and Transfer-Encoding, and raw HTTP/1 socket writes, cannot be used on HTTP/2 streams. request.httpVersionMajor identifies the request protocol; see Node's HTTP/2 compatibility API.

onRawRequest and onRequest receive the public safe serve path. The fifth beforeServe argument is a one-shot completion continuation: call it after manual or asynchronous body work. It completes the response once and bypasses another beforeServe pass.

import {Server} from 'node-http-server';

class ApiAndFiles 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 ApiAndFiles({root:'./public'}).deploy();

Static files stream by default. Defining a custom beforeServe hook uses the compatibility buffered path for those responses so bodyRef.value and encodingRef.value can still be modified.

Static bodies reach beforeServe as Buffers; convert them explicitly before string replacement. A custom beforeServe buffers files and bypasses automatic streaming/compression. A hook that calls response.end() directly also bypasses afterServe.

The named RefString export remains available in CommonJS and ESM for hook compatibility.

Logging

Set log to a file path for one JSON request record per line:

new Server({
    log:'./requests.ndjson'
}).deploy();

The built-in logger preserves the supplied record, adds a timestamp to its own copy, redacts common credential headers, and reports serialization or filesystem errors. Replace logFunction when records need to go somewhere else. Set logBody:true only when storing request bodies is intentional. Treat request logs as sensitive data and protect the destination accordingly.

Development

Install the exact workspace state once with npm ci. Published installs have zero runtime dependencies. The exact vanilla-test@2.1.1 release is the sole direct development dependency and runs the Node-only native V8 coverage workflow.

Vanilla Test 2.1 uses Node's native V8 coverage path and its project-owned reporter. Node's built-in test runner and assertion module execute the behavior suite.

The suite contains 213 unique, focused leaf cases: 57 Unit, 58 Functional, 40 Integration, and 58 Regression. HTTP/2 coverage includes five Config cases and fifteen integration scenarios for negotiation, protocol compatibility, static responses, complete request bodies, hooks, concurrent streams, errors, instance isolation, graceful closure, and cancellation. Each behavior has one owning case. Both the normal runner and coverage use the ordered manifest in test/suites.js. Node's summary also counts the HTTP/2 parent, giving 214 results; generated coverage/node/test-results.json records the associated run's totals and top-level descriptions, while native TAP output includes every nested case.

For a non-unit behavioral pass, run npm run test:behavioral. It selects the Functional, Integration, and Regression cases from the same manifest, including HTTP/2. Its live CLI SPA journey verifies browser routes and assets served by the Node process; it does not execute node-http-server in a browser.

ScriptPurpose
npm startServe the current directory with the CLI
npm testRun all cases discovered by the shared suite manifest
npm run test:unitRun 57 isolated Config and suite-discovery tests from test/unit/
npm run test:functionalRun 58 public HTTP behavior tests from test/functional/
npm run test:behavioralRun 156 non-unit behavioral contracts across Functional, Integration, and Regression
npm run test:integrationRun 40 module, CLI, benchmark, HTTP/2, listener, stream, and filesystem boundary tests from test/integration/
npm run test:regressionRun 58 owned cases for previously fixed failures and security boundaries from test/regression/
npm run test:siteCheck docs pages, local links/fragments, IDs, label/ARIA targets, image alt text, nav state, CSS, and site JavaScript
npm run coverageRun vanilla-test Node coverage gates, write coverage/node/, and refresh measured badge JSON
npm run test:packagePack, install, and smoke-test the publishable package
npm run verifyRun tests, static-doc checks, coverage, and the package smoke test
npm run benchmarkMeasure five validated public paths with the bounded developer profile
npm run benchmark:smokeRun the short real-server measurement profile
npm run benchmark:coreCompare targeted core paths with 9.0.2 using nine alternating samples
npm run benchmark:core:smokeVerify the short core comparison profile
npm run basicRun the basic HTTP example
npm run httpsRun the HTTPS-only example; local certificates are required
npm run bothRun the combined HTTP/HTTPS example; local certificates are required
npm run templateRun the template example
npm run clusterRun the cluster example

GitHub Actions tests Node.js 22.12 and Node.js 24, validates the dependency-free static docs, runs the Node-only vanilla-test coverage gate, smoke-tests the packed npm artifact, measures real HTTP paths on Ubuntu Node 24.18.0, and publishes the reports, badges, and latest benchmark JSON with the static project site from main.

When upgrading from v8 or v9, read MIGRATION.md. Release details are in CHANGELOG.md.

License

MIT