What is Multer?
Multer is an Express middleware for handling multipart/form-data requests — the encoding type used when submitting forms that include file uploads. Without Multer, req.body will be empty and req.file will be undefined when a file is sent.
npm install multer
---
diskStorage vs memoryStorage
| Aspect | diskStorage | memoryStorage |
|---|---|---|
| Storage location | Local file system | Node.js memory (RAM) as Buffer |
| Speed | Slower (disk I/O) | Faster (in-memory) |
| File size limit | Limited by disk space | Limited by available RAM |
| File path | req.file.path | — |
| File buffer | — | req.file.buffer |
| Use case | Before cloud upload (temp storage) | Small files, direct processing |
| Temp cleanup | Must delete manually | Automatic on GC |
Use diskStorage when uploading to Cloudinary — save to a temp folder, upload to cloud, then delete the local file. Use memoryStorage only for small files that are processed in-memory (e.g., reading a CSV).
---
Multer Configuration with diskStorage
import multer from 'multer';
import path from 'path';
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, './public/temp'); // Temp folder on server
},
filename: (req, file, cb) => {
// Use Date.now() + random number to ensure unique filenames
const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
const ext = path.extname(file.originalname);
cb(null, `${file.fieldname}-${uniqueSuffix}${ext}`);
},
});
---
File Filter
Reject non-image files before they reach the server:
const fileFilter = (req, file, cb) => {
const allowedMimeTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'];
if (allowedMimeTypes.includes(file.mimetype)) {
cb(null, true); // Accept file
} else {
cb(new Error('Only JPEG, PNG, and WebP images are allowed'), false); // Reject
}
};
---
Upload Options: single, array, fields
const upload = multer({ storage, fileFilter, limits: { fileSize: 5 * 1024 * 1024 } });
// limits.fileSize: 5MB maximum
// Single file — req.file
router.post('/avatar', upload.single('avatar'), controller);
// Multiple files, same field — req.files (array)
router.post('/gallery', upload.array('images', 5), controller); // max 5
// Multiple fields with different names — req.files (object)
router.post('/register', upload.fields([
{ name: 'avatar', maxCount: 1 },
{ name: 'coverImage', maxCount: 1 },
]), controller);
// Access: req.files?.avatar?.[0], req.files?.coverImage?.[0]
---
Cloudinary Integration
After Multer saves the file to a temp folder, upload it to Cloudinary, get the secure URL, then delete the local temp file:
// src/config/cloudinary.js
import { v2 as cloudinary } from 'cloudinary';
import fs from 'fs';
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
});
export const uploadOnCloudinary = async (localFilePath) => {
if (!localFilePath) return null;
try {
const response = await cloudinary.uploader.upload(localFilePath, {
resource_type: 'auto', // auto-detect image/video/raw
folder: 'myapp', // Cloudinary folder
});
// File uploaded successfully — delete local temp file
fs.unlinkSync(localFilePath);
return response; // response.secure_url, response.public_id
} catch (error) {
// Upload failed — still delete local temp file
if (fs.existsSync(localFilePath)) {
fs.unlinkSync(localFilePath);
}
return null;
}
};
---
Full Controller Flow with File Upload
- Multer middleware reads the
multipart/form-datarequest - Validates file type and size
- Saves to
public/temp/ - Controller reads
req.files?.avatar[0]?.path - Calls
uploadOnCloudinary(localPath) - Receives
{ secure_url, public_id }from Cloudinary - Stores the
secure_urlin MongoDB - Local temp file is deleted (by the upload utility)
---
Handling Multer Errors
// In your error handler middleware:
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({ message: 'File too large. Max 5MB allowed.' });
}
if (err.code === 'LIMIT_FILE_COUNT') {
return res.status(400).json({ message: 'Too many files uploaded.' });
}
if (err.code === 'LIMIT_UNEXPECTED_FILE') {
return res.status(400).json({ message: `Unexpected field: ${err.field}` });
}
}
---
Deleting Old File from Cloudinary on Update
When a user updates their avatar, delete the old image from Cloudinary to avoid orphaned files and storage costs:
export const deleteFromCloudinary = async (publicId) => {
if (!publicId) return null;
return await cloudinary.uploader.destroy(publicId);
};
Store the public_id alongside the URL in your MongoDB document so you can delete it later.