File Uploads
File uploads are a common requirement in web applications. This guide covers best practices for handling file uploads in Express.js applications, including different upload libraries, storage options, and implementation approaches.
File Upload Libraries
Section titled “File Upload Libraries”Multer vs express-fileupload
Section titled “Multer vs express-fileupload”There are two popular libraries for handling file uploads in Express:
Multer is the most popular and widely-used file upload middleware for Express. It’s built on top of busboy
and offers excellent performance and flexibility.
Advantages:
- High performance and memory efficient
- Extensive configuration options
- Built-in field filtering and validation
- Strong community support
- Flexible storage options (memory, disk, custom)
Best for: Production applications requiring performance and flexibility
express-fileupload is a simpler alternative that’s easier to set up but less performant for large files.
Advantages:
- Simple API and setup
- Built-in file moving capabilities
- Good for small to medium files
Disadvantages:
- Loads entire file into memory
- Less performant for large files
- Fewer configuration options
Best for: Simple applications with small file uploads
Storage Configuration
Section titled “Storage Configuration”Installation
Section titled “Installation”npm install multernpm install cloudinary multer-storage-cloudinarynpm install @types/multer # If using TypeScript
Multer Configuration Options
Section titled “Multer Configuration Options”// config/multer.jsconst multer = require('multer');const path = require('path');const { validateFile } = require('../utils/fileValidation');
const storage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, 'uploads/'); // Local directory }, filename: (req, file, cb) => { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname)); }});
const upload = multer({ storage: storage, limits: { fileSize: 5 * 1024 * 1024, // 5MB limit files: 5 }, fileFilter: validateFile});
module.exports = upload;
// config/cloudinaryMulter.jsconst multer = require('multer');const { CloudinaryStorage } = require('multer-storage-cloudinary');const { cloudinary } = require('./cloudinary');const { validateFile } = require('../utils/fileValidation');
const storage = new CloudinaryStorage({ cloudinary: cloudinary, params: { folder: 'uploads', allowed_formats: ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx'] }});
const upload = multer({ storage: storage, limits: { fileSize: 10 * 1024 * 1024, // 10MB for cloud storage files: 5 }, fileFilter: validateFile});
module.exports = upload;
Cloudinary Integration
Section titled “Cloudinary Integration”Cloudinary is recommended for production applications as it provides cloud storage, automatic optimization, and CDN delivery.
// config/cloudinary.jsconst cloudinary = require('cloudinary').v2;
// Configure Cloudinarycloudinary.config({ cloud_name: process.env.CLOUDINARY_CLOUD_NAME, api_key: process.env.CLOUDINARY_API_KEY, api_secret: process.env.CLOUDINARY_API_SECRET});
module.exports = { cloudinary };
Implementation Approaches
Section titled “Implementation Approaches”There are two ways to handle file uploads in your application:
Middleware Approach (Cloudinary Storage)
Section titled “Middleware Approach (Cloudinary Storage)”Files are automatically uploaded to cloud storage during request processing:
// Uses CloudinaryStorage - files uploaded directly in middlewareconst upload = multer({ storage: cloudinaryStorage });
router.post('/register', upload.single('avatar'), userController.register);
// In controller, file is already in cloudexports.register = (req, res) => { const avatarUrl = req.file?.path; // Cloudinary URL available immediately // Business logic here...};
Pros: Simple, automatic, less code Cons: Always uploads even if business logic fails
Helper Approach (Disk Storage) - Recommended
Section titled “Helper Approach (Disk Storage) - Recommended”Files are temporarily stored on disk, then manually uploaded to cloud with full control:
// Uses disk storage - files saved locally firstconst upload = multer({ storage: diskStorage });
router.post('/register', upload.single('avatar'), userController.register);
// In controller, file is on disk - you control cloud uploadexports.register = async (req, res) => { if (req.file) { // File is temporarily on disk at req.file.path const cloudResult = await uploadToCloudinary(req.file.path); const avatarUrl = cloudResult.secure_url;
// Clean up temporary file fs.unlinkSync(req.file.path); } // Business logic here...};
Pros: Full control, conditional uploads, better error handling Cons: More manual work, temporary disk usage
Utility Functions
Section titled “Utility Functions”Cloudinary Helper Functions
Section titled “Cloudinary Helper Functions”// utils/cloudinaryHelpers.jsconst { cloudinary } = require('../config/cloudinary');const fs = require('fs');
const uploadToCloudinary = async (filePath, options = {}) => { try { const uploadOptions = { folder: 'uploads', resource_type: 'auto', ...options };
const result = await cloudinary.uploader.upload(filePath, uploadOptions);
// Clean up temporary file if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); }
return result.secure_url; } catch (error) { // Clean up temporary file even if upload fails if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } throw error; }};
const deleteFromCloudinary = async (publicId, resourceType = 'auto') => { const result = await cloudinary.uploader.destroy(publicId, { resource_type: resourceType }); return result;};
module.exports = { uploadToCloudinary, deleteFromCloudinary };
Upload Error Handling
Section titled “Upload Error Handling”// middleware/uploadErrorHandler.jsconst multer = require('multer');
const handleUploadError = (error, req, res, next) => { if (error instanceof multer.MulterError) { let message = 'Upload error';
switch (error.code) { case 'LIMIT_FILE_SIZE': message = 'File too large. Maximum size is 5MB'; break; case 'LIMIT_FILE_COUNT': message = 'Too many files'; break; case 'LIMIT_UNEXPECTED_FILE': message = 'Unexpected field name'; break; default: message = error.message; }
throw new Error(message); }
if (error.code === 'INVALID_FILE_TYPE') { throw new Error(error.message); }
next(error);};
module.exports = { handleUploadError };
Practical Implementation
Section titled “Practical Implementation”User Registration with Profile Picture
Section titled “User Registration with Profile Picture”// controllers/userController.jsconst { StatusCodes } = require('http-status-codes');const userService = require('../services/userService');
exports.register = async (req, res) => { const { name, email, password } = req.body; const profilePicture = req.file;
const user = await userService.register({ name, email, password, profilePicture });
return res.status(StatusCodes.CREATED).json({ statusCode: StatusCodes.CREATED, message: 'User registered successfully', data: { user: { id: user.id, name: user.name, email: user.email, profilePicture: user.profilePicture } } });};
// services/userService.jsconst { uploadToCloudinary } = require('../utils/cloudinaryHelpers');
exports.register = async ({ name, email, password, profilePicture }) => { // Check if user already exists const existingUser = await User.findOne({ email }); if (existingUser) { throw new Error('User already exists'); }
// Handle profile picture upload if provided let profilePictureUrl = null; if (profilePicture) { profilePictureUrl = await uploadToCloudinary(profilePicture.path, { folder: 'profile-pictures' }); }
// Hash password and create user const hashedPassword = await bcrypt.hash(password, 12); const user = await User.create({ name, email, password: hashedPassword, profilePicture: profilePictureUrl });
return user;};
Route Setup with Error Handling
Section titled “Route Setup with Error Handling”// routes/userRoutes.jsconst express = require('express');const upload = require('../config/multer');const userController = require('../controllers/userController');const { handleUploadError } = require('../middleware/uploadErrorHandler');
const router = express.Router();
// Routes with integrated error handlingrouter.post('/register', upload.single('profilePicture'), handleUploadError, userController.register);
router.put('/profile', upload.single('avatar'), handleUploadError, userController.updateProfile);
module.exports = router;
File Validation
Section titled “File Validation”File Type and Format Validation
Section titled “File Type and Format Validation”// utils/fileValidation.jsconst path = require('path');
const validateFile = (req, file, cb) => { const imageTypes = /jpeg|jpg|png|gif|webp/; const documentTypes = /pdf|doc|docx|txt|rtf/; const videoTypes = /mp4|avi|mov|wmv|mkv/;
const extname = path.extname(file.originalname).toLowerCase(); const mimetype = file.mimetype.toLowerCase();
const isImage = imageTypes.test(extname) && mimetype.startsWith('image/'); const isDocument = documentTypes.test(extname) && (mimetype.startsWith('application/') || mimetype.startsWith('text/')); const isVideo = videoTypes.test(extname) && mimetype.startsWith('video/');
if (isImage || isDocument || isVideo) { return cb(null, true); }
const error = new Error(`Invalid file type: ${extname}. Allowed: images, documents, videos`); error.code = 'INVALID_FILE_TYPE'; cb(error);};
module.exports = { validateFile };
Best Practices
Section titled “Best Practices”-
Use cloud storage for production
- Cloudinary provides automatic optimization and CDN delivery
- Local storage doesn’t scale and creates backup complications
-
Validate business logic before uploading
- Use helper approach to upload only after validating user data
- Avoid orphaned files from failed business operations
-
Implement proper error handling
- Handle upload errors gracefully
- Provide clear error messages to users
-
Organize files properly
- Configuration in
config/
folder - Validation utilities in
utils/
folder - Upload middleware in
middleware/
folder
- Configuration in