Multer is the standard Node.js middleware for handling multipart/form-data — the encoding used for file uploads. It parses the incoming request, extracts the file data, stores it according to your configuration, and attaches the result to req.file or req.files before your route handler runs. In this lesson you will install and configure Multer with disk storage for development, add file type and size filters to reject invalid uploads, and attach the upload middleware to the correct Express routes — keeping the file handling logic clean and reusable across multiple endpoints.
Installation
cd server
npm install multer
The Multer Configuration Module
// server/src/config/multer.js
const multer = require('multer');
const path = require('path');
const fs = require('fs');
// ── Ensure upload directories exist ───────────────────────────────────────────
const UPLOAD_DIRS = ['uploads/avatars', 'uploads/covers'];
UPLOAD_DIRS.forEach(dir => {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
});
// ── Disk storage engine ────────────────────────────────────────────────────────
const diskStorage = multer.diskStorage({
// Where to save the file on the server
destination: (req, file, cb) => {
// Route-based destination — /api/users/avatar vs /api/posts/cover
const folder = file.fieldname === 'avatar' ? 'uploads/avatars' : 'uploads/covers';
cb(null, folder);
},
// What to name the stored file
filename: (req, file, cb) => {
const ext = path.extname(file.originalname).toLowerCase();
const userId = req.user?._id || 'anon';
const unique = `${userId}-${Date.now()}${ext}`;
cb(null, unique); // e.g. '64a1f2b3-1710000000000.jpg'
},
});
// ── File filter — only accept images ──────────────────────────────────────────
const imageFileFilter = (req, file, cb) => {
const allowed = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
if (allowed.includes(file.mimetype)) {
cb(null, true); // accept the file
} else {
cb(new Error(`File type '${file.mimetype}' not allowed. Use JPEG, PNG, WebP, or GIF.`), false);
}
};
// ── Upload instances ───────────────────────────────────────────────────────────
const uploadAvatar = multer({
storage: diskStorage,
fileFilter: imageFileFilter,
limits: {
fileSize: 2 * 1024 * 1024, // 2 MB maximum
files: 1, // one file per request
},
}).single('avatar'); // 'avatar' must match the field name in the FormData
const uploadCover = multer({
storage: diskStorage,
fileFilter: imageFileFilter,
limits: {
fileSize: 5 * 1024 * 1024, // 5 MB for cover images
files: 1,
},
}).single('coverImage');
module.exports = { uploadAvatar, uploadCover };
fieldname argument to .single('avatar') must match the key used in the React FormData.append() call. If React sends formData.append('avatar', file) but Multer is configured with .single('photo'), Multer will not find the file and req.file will be undefined. These field names must match exactly, and by convention they describe what the file represents — not a generic name like ‘file’ or ‘upload’.asyncHandler-compatible wrapper to catch Multer errors and pass them to your global error handler. By default, Multer calls next(err) for errors, but if you wrap Multer in a custom middleware that awaits it using a Promise, you can handle both Multer errors and subsequent handler errors uniformly through the same error handling chain.file.originalname as the stored filename. Original filenames can contain path traversal sequences (../../../etc/passwd), spaces, special characters, and Unicode that can cause filesystem or URL issues. Always generate a new safe filename server-side — using the user ID + timestamp + extension is a common pattern that also prevents filename collisions.Serving Uploaded Files as Static Assets
// server/index.js — serve the uploads directory as static files
const path = require('path');
// Files in uploads/ are accessible at /uploads/filename.jpg
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
// Now an uploaded file at uploads/avatars/64a1f2b3-1710000000.jpg
// is accessible via: http://localhost:5000/uploads/avatars/64a1f2b3-1710000000.jpg
Upload Routes and Controllers
// server/src/controllers/uploadController.js
const asyncHandler = require('../utils/asyncHandler');
const AppError = require('../utils/AppError');
const User = require('../models/User');
// Multer wrapper — converts callback-style to promise
const runMulter = (middleware, req, res) =>
new Promise((resolve, reject) => {
middleware(req, res, (err) => {
if (err) reject(err);
else resolve();
});
});
// @desc Upload user avatar
// @route POST /api/users/avatar
// @access Protected
const uploadAvatarHandler = asyncHandler(async (req, res) => {
if (!req.file) throw new AppError('No file provided', 400);
// Build the public URL for the stored file
const fileUrl = `${process.env.SERVER_URL}/uploads/avatars/${req.file.filename}`;
// Delete old avatar file if replacing (optional cleanup)
// await deleteOldAvatar(req.user.avatar);
// Update user document
const user = await User.findByIdAndUpdate(
req.user._id,
{ avatar: fileUrl },
{ new: true }
).select('-password');
res.json({
success: true,
message: 'Avatar updated successfully',
data: { avatar: fileUrl, user },
});
});
// server/src/routes/users.js
const { uploadAvatar } = require('../config/multer');
const protect = require('../middleware/auth');
router.post('/avatar', protect, uploadAvatar, uploadAvatarHandler);
Handling Multer Errors in errorHandler.js
// server/src/middleware/errorHandler.js — add Multer error cases
// Multer: file too large
if (err.code === 'LIMIT_FILE_SIZE') {
statusCode = 413;
message = `File too large — maximum size is ${Math.round(err.limit / 1024 / 1024)} MB`;
isOperational = true;
}
// Multer: too many files
if (err.code === 'LIMIT_FILE_COUNT') {
statusCode = 400;
message = 'Too many files uploaded at once';
isOperational = true;
}
// Multer: unexpected field name
if (err.code === 'LIMIT_UNEXPECTED_FILE') {
statusCode = 400;
message = `Unexpected file field: '${err.field}' — check the form field name`;
isOperational = true;
}
// File type filter rejection (thrown by fileFilter callback)
if (err.message?.includes('not allowed')) {
statusCode = 415;
message = err.message;
isOperational = true;
}
Common Mistakes
Mistake 1 — Using the original filename as the stored filename
❌ Wrong — filename from the browser used directly:
filename: (req, file, cb) => cb(null, file.originalname)
// '../../../etc/cron.d/evil.sh' stored as-is — path traversal attack!
✅ Correct — always generate a safe server-side filename:
filename: (req, file, cb) => {
const ext = path.extname(file.originalname).toLowerCase();
cb(null, `${req.user._id}-${Date.now()}${ext}`); // ✓ safe
Mistake 2 — Not adding Multer middleware before the route handler
❌ Wrong — Multer is not in the middleware chain:
router.post('/avatar', protect, uploadAvatarHandler);
// req.file is undefined — Multer never ran to parse the file
✅ Correct — Multer middleware before the handler:
router.post('/avatar', protect, uploadAvatar, uploadAvatarHandler); // ✓
Mistake 3 — Not checking req.file exists in the handler
❌ Wrong — accessing req.file.filename when no file was uploaded:
const url = `${process.env.SERVER_URL}/uploads/${req.file.filename}`;
// TypeError: Cannot read properties of undefined — req.file is undefined!
✅ Correct — guard with a null check:
if (!req.file) throw new AppError('No file provided', 400); // ✓
Quick Reference
| Task | Code |
|---|---|
| Install | npm install multer |
| Disk storage | multer.diskStorage({ destination, filename }) |
| File type filter | fileFilter: (req, file, cb) => cb(null, allowed) |
| Size limit | limits: { fileSize: 2 * 1024 * 1024 } |
| Single file upload | multer({ storage, fileFilter, limits }).single('avatar') |
| Access file | req.file — available after Multer middleware runs |
| Serve uploads | app.use('/uploads', express.static('uploads')) |
| File too large error | err.code === 'LIMIT_FILE_SIZE' |