Storing Uploaded Files and Serving Them in Express
Understanding where files are stored and how to serve them with Express
Turning chai into code and ideas into full-stack applications. Sharing lessons from my development journey, one commit at a time.
Where Uploaded Files Are Stored
When users upload files to your Node.js application, you must decide where to store them:
1. Local Storage (Server Filesystem)
Files saved directly on the server's disk.
const multer = require('multer');
const path = require('path');
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/'); // Store in uploads folder
},
filename: (req, file, cb) => {
cb(null, Date.now() + path.extname(file.originalname));
}
});
const upload = multer({ storage });
File location:
project/
├── uploads/
│ ├── 1640000000000.jpg
│ ├── 1640000001000.pdf
│ └── 1640000002000.png
├── server.js
└── package.json
2. External Storage (Cloud Services)
Files uploaded to third-party services like:
AWS S3
Google Cloud Storage
Azure Blob Storage
Cloudinary (for images)
Why use external storage:
Scalability (no server disk limitations)
Reliability (built-in redundancy)
CDN integration (faster global access)
Separation of concerns (storage separate from application)
Local Storage vs External Storage
| Aspect | Local Storage | External Storage |
|---|---|---|
| Setup | Simple, no extra services | Requires cloud account setup |
| Cost | Free (uses server disk) | Pay per GB stored and transferred |
| Scalability | Limited by disk space | Virtually unlimited |
| Performance | Fast for single server | CDN provides global speed |
| Backup | Manual backup needed | Automatic redundancy |
| Server Migration | Must move files manually | Files remain accessible |
| Use Case | Small apps, prototypes | Production apps, large scale |
Serving Static Files in Express
Static files (images, PDFs, videos) need to be accessible via URLs. Express provides middleware to serve files from specific directories.
Basic Static File Serving
const express = require('express');
const app = express();
// Serve files from 'uploads' directory
app.use('/files', express.static('uploads'));
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
How it works:
Files in
uploads/folder are served at/files/*URL pathuploads/1640000000000.jpg→http://localhost:3000/files/1640000000000.jpg
Multiple Static Directories
// Serve different file types from different folders
app.use('/images', express.static('uploads/images'));
app.use('/documents', express.static('uploads/documents'));
app.use('/public', express.static('public'));
Folder structure:
project/
├── uploads/
│ ├── images/
│ │ └── photo.jpg → http://localhost:3000/images/photo.jpg
│ └── documents/
│ └── report.pdf → http://localhost:3000/documents/report.pdf
└── public/
└── style.css → http://localhost:3000/public/style.css
Accessing Uploaded Files via URL
Complete Upload and Serve Example
const express = require('express');
const multer = require('multer');
const path = require('path');
const app = express();
// Configure storage
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
const uniqueName = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, uniqueName + path.extname(file.originalname));
}
});
const upload = multer({ storage });
// Serve uploaded files
app.use('/uploads', express.static('uploads'));
// Upload endpoint
app.post('/upload', upload.single('file'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
const fileUrl = `http://localhost:3000/uploads/${req.file.filename}`;
res.json({
message: 'File uploaded successfully',
filename: req.file.filename,
url: fileUrl
});
});
app.listen(3000);
Upload request:
curl -F "file=@photo.jpg" http://localhost:3000/upload
Response:
{
"message": "File uploaded successfully",
"filename": "1640000000000-123456789.jpg",
"url": "http://localhost:3000/uploads/1640000000000-123456789.jpg"
}
Access file: Open http://localhost:3000/uploads/1640000000000-123456789.jpg in browser.
Folder-Based Storage Structure
Organizing uploads by category or user:
const storage = multer.diskStorage({
destination: (req, file, cb) => {
let folder = 'uploads/';
// Organize by file type
if (file.mimetype.startsWith('image/')) {
folder += 'images/';
} else if (file.mimetype === 'application/pdf') {
folder += 'documents/';
} else {
folder += 'others/';
}
// Create folder if it doesn't exist
const fs = require('fs');
if (!fs.existsSync(folder)) {
fs.mkdirSync(folder, { recursive: true });
}
cb(null, folder);
},
filename: (req, file, cb) => {
cb(null, Date.now() + path.extname(file.originalname));
}
});
Resulting structure:
uploads/
├── images/
│ ├── 1640000000000.jpg
│ └── 1640000001000.png
├── documents/
│ └── 1640000002000.pdf
└── others/
└── 1640000003000.zip
User-Based Organization
destination: (req, file, cb) => {
const userId = req.user.id; // From authentication middleware
const userFolder = `uploads/user-${userId}/`;
const fs = require('fs');
if (!fs.existsSync(userFolder)) {
fs.mkdirSync(userFolder, { recursive: true });
}
cb(null, userFolder);
}
uploads/
├── user-123/
│ ├── profile.jpg
│ └── document.pdf
└── user-456/
└── avatar.png
Security Considerations for Uploads
1. File Type Validation
const upload = multer({
storage: storage,
fileFilter: (req, file, cb) => {
// Allow only images
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Invalid file type. Only JPEG, PNG, and GIF allowed.'));
}
}
});
2. File Size Limits
const upload = multer({
storage: storage,
limits: {
fileSize: 5 * 1024 * 1024 // 5MB limit
}
});
// Handle size limit errors
app.post('/upload', (req, res) => {
upload.single('file')(req, res, (err) => {
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({ error: 'File too large. Max 5MB.' });
}
}
// Handle other errors
});
});
3. Prevent Path Traversal
const path = require('path');
filename: (req, file, cb) => {
// Sanitize original filename
const sanitized = path.basename(file.originalname);
const uniqueName = Date.now() + '-' + sanitized;
cb(null, uniqueName);
}
4. Restrict Direct Access
Serve files only to authenticated users:
// DON'T expose uploads directly
// app.use('/uploads', express.static('uploads')); // Insecure!
// DO authenticate before serving
app.get('/files/:filename', authenticateUser, (req, res) => {
const filename = req.params.filename;
const filepath = path.join(__dirname, 'uploads', filename);
// Check if user owns this file
const fileOwner = getFileOwner(filename); // Your logic
if (fileOwner !== req.user.id) {
return res.status(403).json({ error: 'Access denied' });
}
res.sendFile(filepath);
});
Best Practices
1. Keep Uploads Out of Version Control
.gitignore:
uploads/
*.log
node_modules/
2. Implement Rate Limiting
const rateLimit = require('express-rate-limit');
const uploadLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10 // 10 uploads per window
});
app.post('/upload', uploadLimiter, upload.single('file'), (req, res) => {
// Handle upload
});
3. Log File Operations
app.post('/upload', upload.single('file'), (req, res) => {
console.log({
timestamp: new Date(),
userId: req.user?.id,
filename: req.file.filename,
size: req.file.size,
mimetype: req.file.mimetype
});
res.json({ message: 'Uploaded successfully' });
});
Key Takeaways
Uploaded files can be stored locally (server filesystem) or externally (cloud)
Local storage is simple but limited; external storage scales better
Use
express.static()middleware to serve uploaded files via URLsOrganize files in folders by type, user, or date for better management
Always validate file types, sizes, and filenames
Restrict direct access to sensitive files with authentication
Use non-guessable filenames to prevent unauthorized access
Keep uploads directory out of version control
Implement rate limiting to prevent abuse
Consider external storage for production applications