File Uploads & Storage
Introduction to File Uploads
Handling file uploads is a common requirement in web applications. Whether you're building a profile picture uploader, document management system, or media sharing platform, understanding how to securely and efficiently handle file uploads in Node.js is essential.
Installing Multer
Multer is a Node.js middleware for handling multipart/form-data, primarily used for uploading files:
# Install Multer
npm install multer
# Install additional packages for cloud storage
npm install @aws-sdk/client-s3
npm install @aws-sdk/s3-request-presigner
# Install Sharp for image processing
npm install sharp
Basic File Upload Setup
Configure Multer with disk storage for handling single and multiple file uploads:
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const app = express();
// Ensure upload directory exists
const uploadDir = './uploads';
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
// Configure disk storage
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, uploadDir); // Save files to uploads/ folder
},
filename: function (req, file, cb) {
// Generate unique filename
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const ext = path.extname(file.originalname);
const name = path.basename(file.originalname, ext);
cb(null, name + '-' + uniqueSuffix + ext);
}
});
// Initialize multer with configuration
const upload = multer({
storage: storage,
limits: {
fileSize: 5 * 1024 * 1024 // 5MB limit
}
});
module.exports = upload;
Single File Upload
Handle uploading a single file to your server:
const express = require('express');
const upload = require('./config/multer');
const router = express.Router();
// Single file upload route
router.post('/upload-single', upload.single('avatar'), (req, res) => {
try {
if (!req.file) {
return res.status(400).json({
success: false,
message: 'No file uploaded'
});
}
// File information
const fileInfo = {
originalName: req.file.originalname,
fileName: req.file.filename,
path: req.file.path,
size: req.file.size,
mimetype: req.file.mimetype
};
res.status(200).json({
success: true,
message: 'File uploaded successfully',
file: fileInfo
});
} catch (error) {
res.status(500).json({
success: false,
message: 'Error uploading file',
error: error.message
});
}
});
// Also handle form fields along with file
router.post('/upload-profile', upload.single('avatar'), (req, res) => {
try {
const { username, email } = req.body; // Access form fields
const file = req.file; // Access uploaded file
if (!file) {
return res.status(400).json({ message: 'No file uploaded' });
}
// Process user data and file
res.json({
message: 'Profile updated successfully',
user: { username, email },
avatar: file.filename
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;
Multiple File Uploads
Handle uploading multiple files at once:
const express = require('express');
const upload = require('./config/multer');
const router = express.Router();
// Upload multiple files with same field name
router.post('/upload-multiple', upload.array('photos', 10), (req, res) => {
try {
if (!req.files || req.files.length === 0) {
return res.status(400).json({
success: false,
message: 'No files uploaded'
});
}
// Process multiple files
const filesInfo = req.files.map(file => ({
originalName: file.originalname,
fileName: file.filename,
size: file.size,
mimetype: file.mimetype
}));
res.status(200).json({
success: true,
message: `${req.files.length} files uploaded successfully`,
files: filesInfo
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
// Upload files with different field names
router.post('/upload-mixed',
upload.fields([
{ name: 'avatar', maxCount: 1 },
{ name: 'gallery', maxCount: 5 },
{ name: 'documents', maxCount: 3 }
]),
(req, res) => {
try {
const uploadedFiles = {
avatar: req.files.avatar || [],
gallery: req.files.gallery || [],
documents: req.files.documents || []
};
res.json({
success: true,
message: 'Files uploaded successfully',
files: uploadedFiles
});
} catch (error) {
res.status(500).json({ error: error.message });
}
}
);
module.exports = router;
File Validation
Implement robust file validation for size, type, and other constraints:
const multer = require('multer');
const path = require('path');
// File filter function
const fileFilter = (req, file, cb) => {
// Allowed file extensions
const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
const ext = path.extname(file.originalname).toLowerCase();
// Allowed MIME types
const allowedMimeTypes = [
'image/jpeg',
'image/jpg',
'image/png',
'image/gif',
'image/webp'
];
// Check extension and MIME type
if (allowedExtensions.includes(ext) && allowedMimeTypes.includes(file.mimetype)) {
cb(null, true); // Accept file
} else {
cb(new Error(`Invalid file type. Only ${allowedExtensions.join(', ')} are allowed.`), false);
}
};
// Configure multer with validation
const upload = multer({
storage: storage,
fileFilter: fileFilter,
limits: {
fileSize: 5 * 1024 * 1024, // 5MB
files: 10 // Maximum number of files
}
});
// Error handling middleware
const handleMulterError = (err, req, res, next) => {
if (err instanceof multer.MulterError) {
// Multer-specific errors
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({
success: false,
message: 'File too large. Maximum size is 5MB.'
});
}
if (err.code === 'LIMIT_FILE_COUNT') {
return res.status(400).json({
success: false,
message: 'Too many files. Maximum is 10 files.'
});
}
if (err.code === 'LIMIT_UNEXPECTED_FILE') {
return res.status(400).json({
success: false,
message: 'Unexpected field name in form data.'
});
}
} else if (err) {
// Custom errors (from fileFilter)
return res.status(400).json({
success: false,
message: err.message
});
}
next();
};
module.exports = { upload, handleMulterError };
Memory Storage
Use memory storage for temporary file processing without writing to disk:
const multer = require('multer');
// Configure memory storage
const memoryStorage = multer.memoryStorage();
const uploadToMemory = multer({
storage: memoryStorage,
limits: {
fileSize: 2 * 1024 * 1024 // 2MB limit for memory storage
}
});
// Route using memory storage
router.post('/upload-memory', uploadToMemory.single('image'), (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ message: 'No file uploaded' });
}
// File is in memory as Buffer
const fileBuffer = req.file.buffer;
const fileSize = req.file.size;
const mimetype = req.file.mimetype;
// Process file buffer (e.g., with Sharp for images)
// Or upload directly to cloud storage
res.json({
message: 'File processed successfully',
size: fileSize,
type: mimetype
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
Cloud Storage with AWS S3
Upload files directly to AWS S3 for scalable cloud storage:
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const multer = require('multer');
const path = require('path');
require('dotenv').config();
// Configure AWS S3 client
const s3Client = new S3Client({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
}
});
// Use memory storage for S3 uploads
const uploadToMemory = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 10 * 1024 * 1024 } // 10MB
});
// Upload to S3 function
const uploadToS3 = async (file, folder = 'uploads') => {
const fileName = `${folder}/${Date.now()}-${file.originalname}`;
const params = {
Bucket: process.env.AWS_S3_BUCKET,
Key: fileName,
Body: file.buffer,
ContentType: file.mimetype,
ACL: 'public-read' // Or 'private' for restricted access
};
try {
const command = new PutObjectCommand(params);
await s3Client.send(command);
// Return public URL
const fileUrl = `https://${process.env.AWS_S3_BUCKET}.s3.${process.env.AWS_REGION}.amazonaws.com/${fileName}`;
return fileUrl;
} catch (error) {
throw new Error(`S3 upload failed: ${error.message}`);
}
};
// Route with S3 upload
router.post('/upload-s3', uploadToMemory.single('file'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ message: 'No file uploaded' });
}
// Upload to S3
const fileUrl = await uploadToS3(req.file, 'user-uploads');
res.json({
success: true,
message: 'File uploaded to S3 successfully',
url: fileUrl
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
Serving Static Files
Make uploaded files accessible via URLs:
const express = require('express');
const path = require('path');
const app = express();
// Serve static files from uploads directory
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
// Serve with custom headers and security
app.use('/uploads', (req, res, next) => {
// Set security headers
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Cache-Control', 'public, max-age=86400'); // Cache for 1 day
next();
}, express.static(path.join(__dirname, 'uploads')));
// Protected file serving (authentication required)
app.get('/files/:filename', authenticateUser, (req, res) => {
const filename = req.params.filename;
const filePath = path.join(__dirname, 'uploads', filename);
// Check if file exists
if (!fs.existsSync(filePath)) {
return res.status(404).json({ message: 'File not found' });
}
// Send file with proper headers
res.sendFile(filePath);
});
Image Processing with Sharp
Resize, compress, and transform images before saving:
const sharp = require('sharp');
const multer = require('multer');
const path = require('path');
const fs = require('fs').promises;
// Use memory storage for processing
const uploadToMemory = multer({
storage: multer.memoryStorage(),
fileFilter: (req, file, cb) => {
if (file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('Only image files are allowed'), false);
}
}
});
// Image processing function
const processImage = async (buffer, options = {}) => {
const {
width = 800,
height = 600,
quality = 80,
format = 'jpeg'
} = options;
try {
const processedImage = await sharp(buffer)
.resize(width, height, {
fit: 'inside',
withoutEnlargement: true
})
.toFormat(format, { quality })
.toBuffer();
return processedImage;
} catch (error) {
throw new Error(`Image processing failed: ${error.message}`);
}
};
// Route with image processing
router.post('/upload-image', uploadToMemory.single('image'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ message: 'No image uploaded' });
}
// Process original image
const processedBuffer = await processImage(req.file.buffer, {
width: 1200,
height: 800,
quality: 85
});
// Create thumbnail
const thumbnailBuffer = await processImage(req.file.buffer, {
width: 300,
height: 200,
quality: 70
});
// Save processed images
const filename = `${Date.now()}-${path.parse(req.file.originalname).name}`;
const imagePath = `./uploads/${filename}.jpg`;
const thumbnailPath = `./uploads/${filename}-thumb.jpg`;
await fs.writeFile(imagePath, processedBuffer);
await fs.writeFile(thumbnailPath, thumbnailBuffer);
res.json({
success: true,
message: 'Image uploaded and processed',
files: {
original: `/uploads/${filename}.jpg`,
thumbnail: `/uploads/${filename}-thumb.jpg`
}
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
Practice Exercise
- Create a file upload API with the following endpoints:
- POST /upload/profile-picture - Single image upload with validation
- POST /upload/documents - Multiple file upload (PDFs, max 5 files)
- GET /files/:filename - Serve uploaded files with authentication
- Implement file type validation (images: jpg/png/webp, documents: pdf/docx)
- Add image processing: resize profile pictures to 400x400px and create thumbnails
- Implement proper error handling for all upload scenarios
- Add file deletion endpoint with proper authorization checks
- Optional: Integrate AWS S3 for cloud storage instead of local disk