Two dependencies, one engine. send is a file-streaming library that speaks HTTP caching natively -- ETags, Last-Modified, If-None-Match, Range/206 partial content, 404/403 semantics for missing and hidden files. serve-static wraps send into a middleware that maps req.path onto a directory. Express surfaces the first through res.sendFile/res.download and the second as express.static.

The doc comment in lib/response.js states the relationship plainly: "The code backing res.sendFile() is actually the same code, so HTTP cache support etc is identical."

send (^1.1.0)

Imported by: lib/response.js.

res.sendFile validates the path (absolute, or root given -- rejecting the ambient-relative case that invites traversal bugs), wires the app's etag setting through, and delegates:

lib/response.js
// create file stream
var pathname = encodeURI(path);

// wire application etag option to send
opts.etag = this.app.enabled('etag');
var file = send(req, pathname, opts);

// transfer
sendfile(res, file, opts, function (err) {
  if (done) return done(err);
  if (err && err.code === 'EISDIR') return next();
  // …
});

The local sendfile helper (bottom of lib/response.js) subscribes to send's event vocabulary -- directory, file, stream, end, error, plus on-finished on the response -- to collapse a streaming transfer into one callback that fires exactly once, with aborted connections surfaced as ECONNABORTED. Note the EISDIR line: pointing sendFile at a directory is not an error, it falls through to the next route.

res.download layers Content-Disposition: attachment (built by the content-disposition package) on the same call. examples/downloads/index.js shows the intended error handling -- a callback that distinguishes 404 from transfer failure.

serve-static (^2.2.0)

Imported by: lib/express.js, a single line:

lib/express.js
exports.static = require('serve-static');

express.static(root) is the only middleware Express bundles besides the body parsers. examples/static-files/index.js demonstrates the three compositions that matter -- bare, prefixed (the mount path is stripped before serve-static sees the URL), and multi-root fallthrough:

examples/static-files/index.js
app.use(express.static(path.join(__dirname, 'public')));
// …
app.use('/static', express.static(path.join(__dirname, 'public')));
// …
app.use(express.static(path.join(__dirname, 'public', 'css')));

Stacking works because serve-static calls next() on a miss, letting the next root (or the router) try.

The contract, per the tests

test/express.static.js is one of the largest files in the suite. Guarantees pinned there include: .. segments outside the root answer 404 rather than escaping (traversal tests), dotfiles follow the dotfiles option (allow/deny/ignore), Range requests get 206 with correct byte slices, conditional requests get 304s, fallthrough: false converts misses into errors, and setHeaders runs before the response is committed. If you need to know precisely how static serving behaves at an edge, that file is the reference.

Sources: lib/response.js, lib/express.js, test/express.static.js · last synced 2026-07-27 · a371447 · version 5.2.1