Express File Downloads
In web applications, you often need to allow users to download files like PDFs, images, or data exports. Express.js provides several methods to handle file downloads efficiently. This guide will walk you through implementing file downloads in your Express applications, from basic techniques to more advanced scenarios.
Introduction to File Downloads in Express
File downloads involve sending files from your server to the client's browser, prompting them to save the file locally. Express.js makes this process straightforward with built-in methods and middleware.
When handling file downloads, you need to consider:
- Setting appropriate HTTP headers
- Managing file paths
- Handling large files efficiently
- Providing proper error handling
- Implementing security measures
Basic File Downloads
The simplest way to send a file for download in Express is using the res.download()
method.
Basic Syntax
res.download(path, [filename], [options], [callback])
path
: The path to the file on your serverfilename
(optional): The name that will appear in the download dialogoptions
(optional): Configuration optionscallback
(optional): Function called after download completes or errors
Simple Download Example
const express = require('express');
const app = express();
const path = require('path');
app.get('/download', (req, res) => {
const filePath = path.join(__dirname, 'files', 'document.pdf');
res.download(filePath);
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
In this example, when a user visits /download
, the server sends the PDF file, and the browser displays a download dialog with the filename "document.pdf".
Customizing the Download Filename
You can specify a custom filename that will appear in the user's download dialog:
app.get('/download/report', (req, res) => {
const filePath = path.join(__dirname, 'files', 'quarterly-report.pdf');
res.download(filePath, 'Q3-2023-Report.pdf');
});
Even though the file on the server is named "quarterly-report.pdf", the user will download it as "Q3-2023-Report.pdf".
Error Handling for Downloads
It's important to handle errors that might occur during downloads:
app.get('/download/report', (req, res) => {
const filePath = path.join(__dirname, 'files', 'quarterly-report.pdf');
res.download(filePath, 'Q3-2023-Report.pdf', (err) => {
if (err) {
// Handle errors, like file not found
if (!res.headersSent) {
res.status(404).send('File not found');
}
}
});
});
The callback function helps you handle errors, particularly when the file doesn't exist or can't be accessed.
Alternative Methods for File Serving
Express offers other methods for sending files to clients:
Using res.sendFile()
app.get('/view/image', (req, res) => {
const imagePath = path.join(__dirname, 'images', 'landscape.jpg');
res.sendFile(imagePath);
});
The sendFile()
method sends a file but doesn't force a download. The browser will display the file if it can (like images) rather than download it.
Using res.attachment()
If you want more control over the process, you can use res.attachment()
combined with file streaming:
app.get('/download/large-file', (req, res) => {
const filePath = path.join(__dirname, 'files', 'large-document.pdf');
const fs = require('fs');
// Set file to be an attachment with a specified name
res.attachment('important-document.pdf');
// Stream the file to the response
const fileStream = fs.createReadStream(filePath);
fileStream.pipe(res);
});
This approach is particularly useful for large files as it streams the data rather than loading the entire file into memory.
Handling Dynamic Files and Generated Content
Sometimes, you need to generate files dynamically before downloading:
Generating CSV Files for Download
const createCsvStringifier = require('csv-writer').createObjectCsvStringifier;
const fs = require('fs');
const path = require('path');
app.get('/export/users', async (req, res) => {
// Assume this function fetches users from a database
const users = await fetchUsers();
const csvStringifier = createCsvStringifier({
header: [
{id: 'id', title: 'ID'},
{id: 'name', title: 'Name'},
{id: 'email', title: 'Email'}
]
});
const csvContent = csvStringifier.getHeaderString() +
csvStringifier.stringifyRecords(users);
// Set headers for CSV download
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', 'attachment; filename="users.csv"');
// Send the CSV data
res.send(csvContent);
});
In this example, we generate a CSV file on-the-fly from database data, then send it as a download.
Conditional Downloads and Authentication
In real applications, you'll often need to check permissions before allowing downloads:
app.get('/download/protected-document', (req, res) => {
// Check if user is authenticated
if (!req.isAuthenticated()) {
return res.status(401).send('Authentication required');
}
// Check if user has permission to access this file
if (!userHasAccess(req.user, 'protected-document')) {
return res.status(403).send('Access denied');
}
const filePath = path.join(__dirname, 'protected', 'confidential.pdf');
res.download(filePath, 'confidential.pdf');
});
Streaming Large Files
For large files, streaming is more efficient than loading the entire file into memory:
const fs = require('fs');
const path = require('path');
app.get('/download/video', (req, res) => {
const videoPath = path.join(__dirname, 'videos', 'tutorial.mp4');
// Get file stats
fs.stat(videoPath, (err, stats) => {
if (err) {
return res.status(404).send('File not found');
}
// Set headers
res.setHeader('Content-Length', stats.size);
res.setHeader('Content-Type', 'video/mp4');
res.setHeader('Content-Disposition', 'attachment; filename=tutorial.mp4');
// Stream the file
const stream = fs.createReadStream(videoPath);
stream.pipe(res);
});
});
Tracking Downloads
You might want to track file downloads for analytics:
app.get('/download/:fileId', async (req, res) => {
const fileId = req.params.fileId;
try {
// Get file info from database
const fileInfo = await getFileInfoFromDatabase(fileId);
if (!fileInfo) {
return res.status(404).send('File not found');
}
const filePath = path.join(__dirname, fileInfo.storagePath);
// Log the download
await logDownload({
fileId,
userId: req.user?.id || 'anonymous',
timestamp: new Date(),
ipAddress: req.ip
});
// Send the file
res.download(filePath, fileInfo.originalName);
} catch (error) {
console.error('Download error:', error);
res.status(500).send('Server error while processing download');
}
});
Security Considerations
When implementing file downloads, consider these security practices:
- Validate file paths: Prevent directory traversal attacks by validating file paths.
- Check permissions: Ensure users can only download files they have access to.
- Scan for malware: For uploaded files that others can download, consider scanning for malware.
- Rate limiting: Implement rate limiting to prevent abuse.
Example of path validation:
const path = require('path');
app.get('/download/:filename', (req, res) => {
// Get requested filename
const filename = req.params.filename;
// Validate filename (allow only alphanumeric characters, hyphens and extensions)
if (!filename.match(/^[\w-]+\.\w+$/)) {
return res.status(400).send('Invalid filename');
}
const rootDir = path.join(__dirname, 'public/downloads');
const filePath = path.join(rootDir, filename);
// Prevent directory traversal attack
if (!filePath.startsWith(rootDir)) {
return res.status(403).send('Access denied');
}
res.download(filePath);
});
Complete Download Management Example
Here's a more complete example incorporating many of the concepts we've covered:
const express = require('express');
const path = require('path');
const fs = require('fs');
const rateLimit = require('express-rate-limit');
const app = express();
// Set up rate limiting
const downloadLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // limit each IP to 10 downloads per window
message: 'Too many download requests, please try again later'
});
// Middleware to check authentication (simplified example)
function isAuthenticated(req, res, next) {
if (req.session && req.session.user) {
return next();
}
res.status(401).send('Please log in to download files');
}
// Public downloads - limited rate
app.get('/downloads/public/:filename', downloadLimiter, (req, res) => {
// Sanitize and validate filename
const filename = path.basename(req.params.filename);
const filePath = path.join(__dirname, 'public/downloads', filename);
// Check if file exists
fs.access(filePath, fs.constants.F_OK, (err) => {
if (err) {
return res.status(404).send('File not found');
}
// Log download
console.log(`Public download: ${filename} by IP ${req.ip}`);
// Send file
res.download(filePath, filename, (err) => {
if (err) {
console.error(`Download error for ${filename}:`, err);
if (!res.headersSent) {
res.status(500).send('Error during file download');
}
}
});
});
});
// Protected downloads - authentication required
app.get('/downloads/protected/:filename', isAuthenticated, (req, res) => {
const filename = path.basename(req.params.filename);
const filePath = path.join(__dirname, 'protected/downloads', filename);
// Check user permissions (simplified)
if (!userHasAccessToFile(req.session.user, filename)) {
return res.status(403).send('You do not have permission to download this file');
}
// Check if file exists
fs.access(filePath, fs.constants.F_OK, (err) => {
if (err) {
return res.status(404).send('File not found');
}
// Log download with user info
console.log(`Protected download: ${filename} by user ${req.session.user.id}`);
// Send file with custom filename
const downloadName = `${req.session.user.company}-${filename}`;
res.download(filePath, downloadName);
});
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
// Helper function to check permissions (would connect to your authorization system)
function userHasAccessToFile(user, filename) {
// Implementation would depend on your authorization system
return true; // Simplified for example
}
Summary
Express.js provides versatile options for handling file downloads in web applications:
res.download()
is the most straightforward method for standard downloadsres.sendFile()
works well for displaying files directly in the browser- Streaming with
res.attachment()
is ideal for large files - Always include proper error handling for your download routes
- Implement appropriate security measures to protect files and prevent abuse
- Consider user permissions, rate limiting, and path validation
File downloads are a common requirement in web applications, and Express provides all the tools you need to implement them effectively and securely.
Additional Resources and Practice Exercises
Resources:
- Express.js Official Documentation on res.download()
- Node.js File System Module Documentation
- Content-Disposition Header MDN Documentation
Practice Exercises:
-
Basic File Server: Create a simple Express application that serves downloadable files from a specific directory.
-
Protected Downloads: Implement a system where users need to be authenticated to download certain files.
-
Download Counter: Create a system that tracks how many times each file has been downloaded and display this information on a dashboard.
-
File Preview: Implement an API that provides both a preview mode (using
res.sendFile()
) and a download mode (usingres.download()
) for PDF and image files. -
ZIP on Demand: Create an endpoint that compresses multiple files into a ZIP archive on-the-fly and sends it as a download.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)