serve-static

Express-maintained middleware for serving static files over HTTP, with caching, ETags, and range-request support built in.

Library
npm
v2.2.1
1,423stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
47/100Fair
Development Activity0
Maintenance32
Community68
Maturity60
Momentum28

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
75/100Good
Architecture75
Code Quality78
Innovation80
Learning Curve65

serve-static is the Node.js middleware that Express itself uses under express.static() to serve files straight from a directory on disk. Given a root folder and a request, it resolves the file, streams it to the response, and calls next() (rather than sending a 404) whenever no file matches, so it can be stacked with other middleware or used as a fallback in a routing chain.

Rather than reimplementing file streaming, it delegates the heavy lifting — range requests, conditional GETs, ETags, MIME typing — to the send package, and focuses its own logic on Express/Connect-style middleware wiring: option normalization, directory-listing behavior (redirect-to-trailing-slash or 404), dotfile handling, and error fall-through semantics. It ships as a single small file with no build step, making it one of the most widely depended-on packages in the Node.js ecosystem.

What You Get

  • A single serveStatic(root, options) factory that returns a standard (req, res, next) middleware function usable in Express, Connect, or vanilla http.createServer handlers
  • Built-in HTTP caching support — ETag, Last-Modified, Cache-Control with maxAge/immutable, and conditional-request handling inherited from send
  • Configurable dotfile policy (allow/deny/ignore) so requests for hidden files can be permitted, rejected, or treated as not found
  • Directory-request handling that either 301-redirects to a trailing slash and serves an index file, or falls through/404s when redirect is disabled
  • A setHeaders(res, path, stat) hook for attaching custom response headers per file (e.g. forcing downloads via Content-Disposition)
  • Fall-through error semantics so a missing file calls next() instead of terminating the request, letting multiple static roots or routes stack

Common Use Cases

  • Serving a public/ or build/ folder of static assets (HTML, CSS, JS, images) from an Express app via app.use(serveStatic(...))
  • Stacking multiple static directories with fallback (e.g. an optimized-assets folder checked before a general public folder)
  • Forcing file downloads instead of inline rendering by setting Content-Disposition through the setHeaders hook
  • Serving static files from a bare Node.js http.createServer without pulling in a full framework
  • Applying different cache lifetimes per file type (e.g. no caching for HTML, long maxAge for hashed JS/CSS bundles)

Under The Hood

Architecture The module is a single ~200-line file exporting one factory function, serveStatic(root, options), that returns a standard Express/Connect middleware (req, res, next). It delegates the actual file-serving and streaming logic entirely to the send package, acting as a thin adapter that wires send’s event-driven stream interface (directory, headers, error, file events) into Express’s fallthrough and error-forwarding conventions. Directory-listing behavior is implemented via two small closures — a redirect listener and a not-found listener — chosen once at setup based on the redirect option, keeping branching logic out of the hot request path. Each call to serveStatic() produces an independent middleware closure with no shared mutable state across requests, so if send’s streaming contract ever changed, this module would need a substantial rewrite since it holds no independent file-system logic of its own.

Tech Stack Plain CommonJS Node.js (engine >= 18), with no build step or bundler — the package ships its raw source (index.js plus LICENSE) directly to npm. Runtime dependencies are all small, single-purpose utilities maintained by the same expressjs org: encodeurl, escape-html, parseurl, and send for the actual streaming. Development tooling is ESLint 7 with the standard config, Mocha for the test runner, nyc for coverage (reported to Coveralls), and supertest for HTTP-level integration assertions. No TypeScript or Flow — types aren’t checked statically anywhere in the codebase.

Code Quality The test suite is an 883-line Mocha spec with 19 top-level describe blocks exercising nearly every option (dotfiles, etag, extensions, fallthrough, redirect, setHeaders, index, immutable, maxAge, acceptRanges) through real HTTP requests via supertest rather than mocks, giving thorough behavioral coverage. CI runs the suite through GitHub Actions with coverage reporting, and linting is enforced via npm run lint. Error handling is explicit: bad constructor arguments throw synchronous TypeErrors, and runtime stream errors are forwarded through a fallthrough/forwardError flag that decides whether to call next() or next(err). There is no static type system in place, so correctness relies entirely on the test suite and linter rather than compiler-enforced types.

API Design The public surface is a single default export matching the exact Express/Connect middleware signature, so it drops into app.use() with no adapter code required. Options are a flat, well-documented object — each one explained with its default in the README — and the setHeaders(res, path, stat) hook is the sole extensibility point, letting callers customize per-file headers (like forcing downloads) without subclassing or a configuration DSL. Given the module’s small surface area, documentation is thorough: every option is explained with its default and behavior, backed by four runnable example snippets covering vanilla http, forced downloads, simple Express usage, multiple static roots, and per-extension cache policy.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search