send streams a file to a response and leaves the server to you
Streaming static file server with Range and conditional-GET support
At a glance
- What is it?
- The send library handles the awkward part of serving a static file over HTTP, ranging and conditional requests, dotfile policy and index handling, and it does it by emitting events rather than owning your server. It suits framework authors who want correct file semantics without writing them from scratch.
- Who is it for?
- Adopt send when you are building an HTTP service that serves files and want the Range, conditional GET, etag and dotfile behaviour handled correctly without writing a static file server, since the library covers those cases and exposes everything else as events you can act on. Do not adopt it expecting a ready-made server, because it is a library that pipes into your own response object, and the README points to serve-static for serving whole folders mapped to URLs.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 145 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A library that owns the file, not the server
The first thing to understand is the boundary. Send is described as a library for streaming files from the file system as an HTTP response, and the README's own pointer for whole folders mapped to URLs is the separate `serve-static` package. The distinction matters for anyone choosing between them. Send gives you a `SendStream` for one path, and you pipe it into your response, which means it fits inside a framework that already owns routing, error pages and middleware. It also means you are responsible for the parts it does not do, and the README is explicit about the biggest of those. Send does not perform internal caching, and you should use a reverse proxy cache such as Varnish, or a CDN, for that, with the aside that if your application is small enough to benefit from single-node memory caching, it is small enough that it does not need caching at all.
One call and one pipe
The API is small enough to hold in your head. You call `send(req, path, [options])` to create a new `SendStream` for the given path, where `req` is the Node.js HTTP request and `path` is a urlencoded path to send, and the README is careful to say that the path is urlencoded rather than the actual file system path. The result is piped into the response with the `pipe` method, typically `send(req, path, options).pipe(res)`. A minimal server that sends one file looks like this:
var http = require('http')
var send = require('send')
var server = http.createServer(function onRequest (req, res) {
send(req, '/path/to/index.html')
.pipe(res)
})
server.listen(3000)To serve a directory instead, you parse the URL yourself and pass a root, so that a request for `/foo.txt` serves `/www/public/foo.txt`:
var http = require('http')
var parseUrl = require('parseurl')
var send = require('send')
var server = http.createServer(function onRequest (req, res) {
send(req, parseUrl(req).pathname, { root: '/www/public' })
.pipe(res)
})The dependency list shows the shape of the work, since the package pulls in `range-parser` for the Range header, `fresh` for conditional request freshness checks, `etag` for entity tag generation, `encodeurl` and `escape-html` for safe paths and output, `http-errors` for status codes, `mime-types`, `ms` for time formatting, `on-finished` and `statuses`.
Ranges, etags and the conditional GET options
The conditional request support is the reason most people reach for this library, and it is configured through options rather than assumed. `acceptRanges` enables or disables accepting ranged requests and defaults to true, and disabling it means no `Accept-Ranges` header is sent and the `Range` request header is ignored. `etag` enables or disables etag generation and defaults to true, and `lastModified` enables or disables the `Last-Modified` header, also defaulting to true and using the file system's last modified value. `cacheControl` defaults to true as well, and disabling it makes the `immutable` and `maxAge` options irrelevant. `maxAge` takes a max-age in milliseconds for HTTP caching and defaults to 0, while `immutable` defaults to false and, when set, stops supported clients from making conditional requests during the life of the maxAge, which is why the README says maxAge should also be specified if you enable it. The `start` and `end` options set the byte offsets of the stream and are inclusive, so `end: 3` includes the fourth byte.
Dotfiles, index files and extension fallbacks
Three options decide how paths resolve, and each of them has a policy decision inside it. `dotfiles` sets how dotfiles are treated, where a dotfile is a file or directory beginning with a dot, and the options are `allow` for no special treatment, `deny` to send a 403 for any request for a dotfile, and `ignore` to pretend the dotfile does not exist and return 404. The README notes that the check is done on the path itself without checking whether the path exists on disk, and that if `root` is specified only the dotfiles above the root are checked, so the root itself can live inside a dotfile when set to `deny`. The default is similar to `ignore` with one exception, that it does not ignore files within a directory that begins with a dot, which is kept for backward compatibility. `index` controls directory index files, supporting `index.html` by default, and `extensions` tries appending given extensions in order when a file does not exist, disabled by default and skipped if the requested file already has an extension.
Events are the extension point, not a callback
The `SendStream` is an event emitter, and the six events it emits are where you plug in. There is `error` with the error, `directory` with the response and path when a directory was requested, `file` with the path and stat when a file was requested, `headers` with the response, path and stat just before the headers are set on a file, `stream` with the stream when file streaming has started, and `end` when streaming has completed. The `headers` event is the one that makes the library composable with a framework, because it lets you set response headers per file. The README gives the example of overriding the content type for two custom extensions:
send(req, parseUrl(req).pathname, { root: '/www/public' })
.on('headers', function (res, path) {
switch (extname(path)) {
case '.x-mt':
case '.x-mtt':
res.setHeader('Content-Type', 'application/x-my-type')
break
}
})
.pipe(res)The `directory` event is the other useful one, since it lets you render a directory listing yourself instead of accepting the default.
Error handling switches between automatic and manual
The error model is a single sentence in the README and it carries a real decision. By default, when no `error` listeners are present, an automatic response will be made, otherwise you have full control over the response, which the README describes as being able to show a 5xx page and so on. In practice that means a service that attaches nothing gets sensible behaviour for free, and a service that attaches a listener takes over the 5xx path entirely and is responsible for producing a response. The `directory` event follows the same pattern, since handling it means you have to end the response yourself. For a framework author this is the right default, because the framework already has an error page and a routing layer, and for a small script it is the right default because there is nothing else to fall back on.
Debugging output and the test command
Send uses the `debug` module, and the README shows how to turn its instrumentation on by exporting `__DEBUG__`:
$ DEBUG=send node appThe repository is small in a way that reflects the scope of the job. The published package contains only `LICENSE`, `README.md` and `index.js`, so the implementation is a single file, and the repository root holds `index.js`, `package.json`, `HISTORY.md`, `LICENSE`, `README.md`, an `index.js` entry, a `scripts/` directory with the version history helper, and a `test/` directory. The test runner is Mocha with the spec reporter and the leak check enabled, run with:
$ npm install
$ npm testThe CI variants add coverage through nyc, and the version script regenerates `HISTORY.md` and adds it to the index. The project requires Node 18 or later, is MIT licensed, is funded through Open Collective under the Express project, and was started by TJ Holowaychuk with contributions from Douglas Christopher Wilson, James Wyatt Cready and Jesús Leganés Combarro.
Editorial conclusion
Adopt send when you are building an HTTP service that serves files and want the Range, conditional GET, etag and dotfile behaviour handled correctly without writing a static file server, since the library covers those cases and exposes everything else as events you can act on. Do not adopt it expecting a ready-made server, because it is a library that pipes into your own response object, and the README points to serve-static for serving whole folders mapped to URLs. Verify first how you want errors handled, since with no `error` listener the library makes an automatic response, and with a listener you take over the 5xx behaviour entirely, and note that the last push was on 2026-06-01 with the current release at 1.2.1.
Frequently asked questions
What is the difference between send and serve-static?
Send is a library for streaming a file to an HTTP response, and you pipe it into your own response object. The README points to `serve-static` for serving entire folders mapped to URLs, and the `root` option in send lets you do the same for a single directory.
How do I serve a specific file with send?
Call `send(req, '/path/to/index.html')` and pipe the result into the response, typically `send(req, path, options).pipe(res)`. The path argument is urlencoded, not the actual file system path.
Does send cache files internally?
No. The README states that it does not perform internal caching and that you should use a reverse proxy cache such as Varnish, or a CDN, for that, while noting that an application small enough to benefit from single-node memory caching is small enough not to need caching.
How does send handle dotfiles and directory indexes?
The `dotfiles` option takes `allow`, `deny` or `ignore`, with a default similar to `ignore` that does not ignore files inside a dot directory, for backward compatibility. The `index` option controls index files, supporting `index.html` by default, and `extensions` tries appended extensions in order when a file is missing.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/pillarjs-send)Was this useful?