Send files using Express
By Flavio Copes
Learn how to send a file as a download in Express using the Response.download() method, set a custom filename, and run a callback when done.
Express provides a handy method to transfer a file as attachment: Response.download().
Once a user hits a route that sends a file using this method, browsers will prompt the user for download, instead of showing the file in the page.
Behind the scenes, res.download() sets the Content-Disposition header to attachment, which is what tells the browser to save the file to disk. It also sets the right Content-Type based on the file extension.
app.get('/', (req, res) => res.download('./file.pdf'))
These examples use Express 5, the current major version. It’s what npm install express installs, and it needs Node 18 or newer. res.download() works like it did in Express 4, except for hidden folders, which I cover below.
In the context of an app:
import express from 'express'
const app = express()
app.get('/', (req, res) => res.download('./file.pdf'))
app.listen(3000, () => console.log('Server ready'))
The import line makes this an ES module. Save the file as server.mjs, or keep the .js extension and add "type": "module" to your package.json.
Setting a custom filename
You can set the file to be sent with a custom filename:
res.download('./file.pdf', 'user-facing-filename.pdf')
The first argument is the file on your disk. The second is the name the user sees in the download dialog. This is handy when your files are stored with generated names like report-2xk39a.pdf but you want the user to receive invoice.pdf.
Running code after the download
This method provides a callback function which you can use to execute code once the file has been sent:
res.download('./file.pdf', 'user-facing-filename.pdf', (err) => {
if (err) {
//handle error
return
} else {
//do something
}
})
When you pass a callback, Express leaves the error response to you. If the file is missing and your callback sends nothing, the request hangs and the browser keeps waiting.
One thing to check inside the error handler is res.headersSent. If the transfer failed midway, part of the response already reached the browser, and you can’t send a fresh error page any more. You can only log the problem at that point.
Be careful with relative paths
A path like ./file.pdf is resolved from the directory where you launched node, not from the directory of your JavaScript file.
Start the app from a different folder, for example with node src/server.mjs from the project root, and the download fails with an ENOENT (file not found) error.
The fix is to build an absolute path from the folder that contains the current file. In an ES module that’s import.meta.dirname, the equivalent of __dirname in CommonJS, and every supported Node release has it:
import path from 'node:path'
app.get('/', (req, res) => {
res.download(path.join(import.meta.dirname, 'file.pdf'))
})
Now the file is found no matter where you start the server from.
In Express 5, a path that goes through a hidden folder (one whose name starts with a dot, like .data) fails with a 404. Express 4 served it. To allow it, pass the dotfiles option:
res.download(path.join(import.meta.dirname, '.data', 'file.pdf'), {
dotfiles: 'allow',
})
One last thing. res.download() forces a download. If you want the browser to display the file inline when it can, like a PDF in the built-in viewer, use res.sendFile() instead. Give it an absolute path too, or set its root option, otherwise it throws a TypeError.
Want me to talk about your product? You can sponsor this site.