Express File Downloads
Introduction
File downloads are a common requirement in web applications. From PDF reports to images, CSV data exports, or software packages, providing a way for users to download files is essential functionality. Express.js makes implementing download capabilities straightforward through its response object methods and middleware.
In this tutorial, you'll learn various approaches to enable file downloads in your Express applications. We'll cover both static file downloads and dynamically generated content, along with best practices for handling different file types and large files.
Basic File Downloads
Using res.download()
Express provides a convenient res.download()
method specifically designed for file downloads. This method sets the appropriate HTTP headers and sends the specified file to the client.
const express = require('express');
const path = require('path');
const app = express();
app.get('/download', (req, res) => {
const filePath = path.join(__dirname, 'files', 'sample.pdf');
res.download(filePath, 'user-facing-filename.pdf', (err) => {
if (err) {
// Handle error, but keep in mind the response may be partially sent
console.error('Download error:', err);
} else {
// File was sent successfully
console.log('File downloaded successfully');
}
});
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
The res.download()
method accepts three parameters:
- The file path on your server
- (Optional) The filename that will be shown to the user when downloading
- (Optional) A callback function that runs when the transfer is complete or if an error occurs
When a user visits /download
, they'll receive a prompt to download the PDF file with the name "user-facing-filename.pdf".
Using res.sendFile()
Another way to serve file downloads is using res.sendFile()
. The difference between sendFile()
and download()
is that sendFile()
doesn't set the Content-Disposition
header to "attachment" by default, which means browsers might attempt to display the file (like opening a PDF or image) rather than downloading it.
app.get('/view-or-download', (req, res) => {
const filePath = path.join(__dirname, 'files', 'sample.pdf');
res.sendFile(filePath);
});
To force a download with sendFile()
, you can manually set the Content-Disposition
header:
app.get('/force-download', (req, res) => {
const filePath = path.join(__dirname, 'files', 'sample.pdf');
res.setHeader('Content-Disposition', 'attachment; filename="downloaded-file.pdf"');
res.sendFile(filePath);
});
Setting Proper MIME Types
When sending files, it's important to set the correct content type so browsers know how to handle the file:
app.get('/download-with-mime', (req, res) => {
const filePath = path.join(__dirname, 'files', 'data.csv');
// Set the appropriate MIME type
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', 'attachment; filename="exported-data.csv"');
res.sendFile(filePath);
});
Common MIME types include:
- PDF:
application/pdf
- CSV:
text/csv
- Excel:
application/vnd.ms-excel
orapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet
- Images:
image/png
,image/jpeg
, etc. - ZIP:
application/zip
Dynamic File Generation and Download
Sometimes you need to generate files on the fly before allowing users to download them. Here's an example that creates a CSV file dynamically:
const fs = require('fs');
app.get('/generate-csv', (req, res) => {
// Sample data
const data = [
{ name: 'John', email: '[email protected]', age: 30 },
{ name: 'Jane', email: '[email protected]', age: 25 },
{ name: 'Bob', email: '[email protected]', age: 40 }
];
// Create CSV content
let csvContent = 'Name,Email,Age\n';
data.forEach(person => {
csvContent += `${person.name},${person.email},${person.age}\n`;
});
// Set headers for file download
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', 'attachment; filename="users.csv"');
// Send the CSV data
res.send(csvContent);
});
For more complex file generation like PDFs or Excel files, you might use libraries such as pdfkit
, xlsx
, or docx
:
const PDFDocument = require('pdfkit');
app.get('/generate-pdf', (req, res) => {
// Create a new PDF document
const doc = new PDFDocument();
// Set headers for file download
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', 'attachment; filename="generated-document.pdf"');
// Pipe the PDF document to the response
doc.pipe(res);
// Add content to the PDF
doc.fontSize(25).text('Hello World!', 100, 100);
doc.image('path/to/image.png', 100, 160, { width: 300 });
doc.addPage().fontSize(16).text('New page content', 100, 100);
// Finalize the PDF and end the stream
doc.end();
});
Handling Large File Downloads with Streams
When dealing with large files, streaming is the preferred approach as it avoids loading the entire file into memory:
const fs = require('fs');
app.get('/download-large-file', (req, res) => {
const filePath = path.join(__dirname, 'files', 'large-video.mp4');
// Get file stats
fs.stat(filePath, (err, stats) => {
if (err) {
console.error(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="video.mp4"');
// Create read stream and pipe to response
const fileStream = fs.createReadStream(filePath);
fileStream.pipe(res);
// Handle errors
fileStream.on('error', (error) => {
console.error('Stream error:', error);
res.end();
});
});
});
Progress Tracking for Large Downloads
For large file downloads, you might want to track progress. This requires a combination of front-end and back-end implementation:
app.get('/download-with-progress', (req, res) => {
const filePath = path.join(__dirname, 'files', 'large-file.zip');
fs.stat(filePath, (err, stats) => {
if (err) {
return res.status(404).send('File not found');
}
// Set headers
res.setHeader('Content-Length', stats.size);
res.setHeader('Content-Type', 'application/zip');
res.setHeader('Content-Disposition', 'attachment; filename="download.zip"');
let bytesSent = 0;
const fileStream = fs.createReadStream(filePath);
// Track bytes sent
fileStream.on('data', (chunk) => {
bytesSent += chunk.length;
console.log(`Progress: ${(bytesSent / stats.size * 100).toFixed(2)}%`);
});
fileStream.on('end', () => {
console.log('Download complete');
});
fileStream.on('error', (error) => {
console.error('Download error:', error);
if (!res.headersSent) {
res.status(500).send('Error downloading file');
}
});
// Send the file
fileStream.pipe(res);
});
});
For the front-end, you can use the fetch
API with a ReadableStream
to track download progress:
// Front-end code example
async function downloadWithProgress(url) {
const response = await fetch(url);
const reader = response.body.getReader();
const contentLength = +response.headers.get('Content-Length');
let receivedLength = 0;
const chunks = [];
while(true) {
const {done, value} = await reader.read();
if (done) {
break;
}
chunks.push(value);
receivedLength += value.length;
// Calculate and display progress
const progress = Math.round((receivedLength / contentLength) * 100);
console.log(`Downloaded ${progress}%`);
// Update UI with progress
document.getElementById('progress-bar').value = progress;
}
// Concatenate chunks into a single Uint8Array
const chunksAll = new Uint8Array(receivedLength);
let position = 0;
for(let chunk of chunks) {
chunksAll.set(chunk, position);
position += chunk.length;
}
// Create a blob and trigger download
const blob = new Blob([chunksAll]);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'downloaded-file';
a.click();
URL.revokeObjectURL(url);
}
Managing Protected Downloads
For protected files that require authentication or authorization:
// Middleware to check if user is authenticated
const isAuthenticated = (req, res, next) => {
if (req.session && req.session.user) {
return next();
}
res.status(401).send('Authentication required');
};
// Middleware to check specific permissions
const hasDownloadPermission = (req, res, next) => {
if (req.session.user && req.session.user.permissions.includes('download')) {
return next();
}
res.status(403).send('Not authorized to download files');
};
// Route with multiple middleware checks
app.get('/protected-download/:fileId', isAuthenticated, hasDownloadPermission, (req, res) => {
const fileId = req.params.fileId;
// Look up file path based on ID (could be from a database)
const filePath = path.join(__dirname, 'protected-files', `${fileId}.pdf`);
// Check if file exists
fs.access(filePath, fs.constants.F_OK, (err) => {
if (err) {
return res.status(404).send('File not found');
}
// Log the download activity
console.log(`User ${req.session.user.id} downloaded file ${fileId}`);
// Send the file as a download
res.download(filePath, `document-${fileId}.pdf`);
});
});
Real-World Application: File Download Manager
Let's build a more complete example of a file download manager that combines several concepts:
const express = require('express');
const path = require('path');
const fs = require('fs');
const multer = require('multer');
const app = express();
// Set up file storage for uploads
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
cb(null, Date.now() + '-' + file.originalname);
}
});
const upload = multer({ storage });
// Simple in-memory database for file records
const fileDatabase = [];
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
// Upload endpoint
app.post('/upload', upload.single('file'), (req, res) => {
if (!req.file) {
return res.status(400).send('No file uploaded');
}
// Create file record
const fileRecord = {
id: Date.now().toString(),
originalName: req.file.originalname,
filename: req.file.filename,
mimetype: req.file.mimetype,
size: req.file.size,
uploadDate: new Date(),
path: req.file.path
};
fileDatabase.push(fileRecord);
res.status(201).json({ success: true, fileId: fileRecord.id });
});
// List available files
app.get('/files', (req, res) => {
// Return sanitized list (without server paths)
const fileList = fileDatabase.map(file => ({
id: file.id,
name: file.originalName,
size: file.size,
type: file.mimetype,
uploadDate: file.uploadDate
}));
res.json(fileList);
});
// Download endpoint
app.get('/download/:fileId', (req, res) => {
const file = fileDatabase.find(f => f.id === req.params.fileId);
if (!file) {
return res.status(404).send('File not found');
}
res.download(file.path, file.originalName, (err) => {
if (err) {
console.error('Download error:', err);
// If headers have not been sent yet, we can return an error response
if (!res.headersSent) {
return res.status(500).send('Error downloading file');
}
}
});
});
// Stream video files
app.get('/stream/:fileId', (req, res) => {
const file = fileDatabase.find(f => f.id === req.params.fileId);
if (!file) {
return res.status(404).send('File not found');
}
if (!file.mimetype.startsWith('video/')) {
return res.status(400).send('File is not a video');
}
const range = req.headers.range;
if (!range) {
// No range requested, send entire file
res.setHeader('Content-Type', file.mimetype);
fs.createReadStream(file.path).pipe(res);
return;
}
// Handle range request for video streaming
const fileSize = file.size;
const parts = range.replace(/bytes=/, '').split('-');
const start = parseInt(parts[0], 10);
const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
const chunkSize = (end - start) + 1;
res.status(206); // Partial Content
res.setHeader('Content-Range', `bytes ${start}-${end}/${fileSize}`);
res.setHeader('Accept-Ranges', 'bytes');
res.setHeader('Content-Length', chunkSize);
res.setHeader('Content-Type', file.mimetype);
fs.createReadStream(file.path, { start, end }).pipe(res);
});
// Start server
app.listen(3000, () => {
console.log('File download manager running on port 3000');
// Create uploads directory if it doesn't exist
if (!fs.existsSync('uploads')) {
fs.mkdirSync('uploads');
}
});
This application provides:
- File upload capability
- Listing of available files
- Regular downloads of any file
- Video streaming with range requests
- Simple "database" to track file information
Summary
Express.js offers several methods to handle file downloads effectively:
res.download()
- The simplest approach for straightforward file downloadsres.sendFile()
- For sending files that might be displayed in the browser- Streaming with
fs.createReadStream()
- For efficient handling of large files - Dynamic file generation - Creating files on-the-fly before sending them
When implementing file downloads, remember these best practices:
- Always set appropriate MIME types and headers
- Use streams for large files to minimize memory usage
- Implement proper error handling for failed downloads
- Add authentication and authorization for protected resources
- Consider tracking download progress for large files
- Validate file paths to prevent directory traversal attacks
- Add logging for download operations, especially in production
Additional Resources
- Express.js Documentation on res.download()
- Node.js File System Documentation
- HTTP Headers for File Downloads
- Stream API in Node.js
Exercise Challenges
-
Basic Download Service: Create a simple Express application that serves downloads from a specific directory.
-
Protected Downloads: Implement a download system where users must be logged in to access certain files.
-
ZIP Archive Generator: Create an endpoint that compresses multiple files into a ZIP archive on-demand and offers it for download.
-
Download Rate Limiting: Add rate limiting to your download endpoints to prevent abuse.
-
Progress Tracking API: Build a download system with a WebSocket connection that notifies the client about download progress in real-time.
By mastering file downloads in Express, you'll be able to implement a wide range of functionality in your web applications, from document sharing to media distribution and data exports.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)