Skip to main content

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

javascript
res.download(path, [filename], [options], [callback])
  • path: The path to the file on your server
  • filename (optional): The name that will appear in the download dialog
  • options (optional): Configuration options
  • callback (optional): Function called after download completes or errors

Simple Download Example

javascript
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:

javascript
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:

javascript
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()

javascript
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:

javascript
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

javascript
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:

javascript
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:

javascript
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:

javascript
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:

  1. Validate file paths: Prevent directory traversal attacks by validating file paths.
  2. Check permissions: Ensure users can only download files they have access to.
  3. Scan for malware: For uploaded files that others can download, consider scanning for malware.
  4. Rate limiting: Implement rate limiting to prevent abuse.

Example of path validation:

javascript
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:

javascript
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 downloads
  • res.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:

Practice Exercises:

  1. Basic File Server: Create a simple Express application that serves downloadable files from a specific directory.

  2. Protected Downloads: Implement a system where users need to be authenticated to download certain files.

  3. Download Counter: Create a system that tracks how many times each file has been downloaded and display this information on a dashboard.

  4. File Preview: Implement an API that provides both a preview mode (using res.sendFile()) and a download mode (using res.download()) for PDF and image files.

  5. 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! :)