File Uploads with Multer
Standard Express body parsers handle text and JSON, but they cannot process file uploads. When a user uploads a profile picture, a PDF report, or a CSV dataset, the browser sends the request as multipart/form-data — a special format that packages binary file data alongside regular form fields. Multer is the most widely used middleware for handling this format in Express applications.
How File Upload Requests Differ
Think of a regular form submission like sending a text letter. A file upload is like sending a parcel — it has packaging (the multipart boundary), multiple compartments (fields and files), and each compartment has its own label and content type. The post office (Express) needs a special handler (Multer) to open and sort parcels correctly.
Regular form POST body: name=Alice&email=alice@example.com Multipart/form-data POST body: ──────boundary1234 Content-Disposition: form-data; name="name" Alice ──────boundary1234 Content-Disposition: form-data; name="avatar"; filename="photo.jpg" Content-Type: image/jpeg [binary file data here...] ──────boundary1234--
Install Multer
npm install multer
Basic Setup: Upload to a Local Folder
const express = require('express');
const multer = require('multer');
const path = require('path');
const app = express();
// Configure where and how to store uploaded files
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/'); // Save files to the "uploads" folder
},
filename: function (req, file, cb) {
// Create a unique filename: timestamp + original extension
const uniqueName = Date.now() + path.extname(file.originalname);
cb(null, uniqueName);
}
});
const upload = multer({ storage });
Create the uploads/ folder in your project root before running the server:
mkdir uploads
Single File Upload
// upload.single('fieldName') — matches the name attribute in the HTML input
app.post('/upload/avatar', upload.single('avatar'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
res.json({
message: 'File uploaded successfully',
filename: req.file.filename,
originalName: req.file.originalname,
size: req.file.size,
mimetype: req.file.mimetype
});
});
The corresponding HTML form:
<form action="/upload/avatar" method="POST" enctype="multipart/form-data"> <input type="file" name="avatar" accept="image/*" /> <button type="submit">Upload</button> </form>
The enctype="multipart/form-data" attribute is required. Without it, the browser sends the filename as text instead of the actual file.
What Multer Adds to req
┌────────────────────────────────────────────────────────────────┐ │ req.file Properties (single upload) │ ├───────────────────┬────────────────────────────────────────────┤ │ fieldname │ 'avatar' (HTML input name) │ │ originalname │ 'profile-photo.jpg' │ │ encoding │ '7bit' │ │ mimetype │ 'image/jpeg' │ │ destination │ 'uploads/' │ │ filename │ '1714300000000.jpg' (your generated name) │ │ path │ 'uploads/1714300000000.jpg' │ │ size │ 204800 (bytes) │ └───────────────────┴────────────────────────────────────────────┘ For text fields alongside the file: req.body For multiple files: req.files (array)
Multiple File Upload
// Accept up to 5 files from the "photos" field
app.post('/upload/gallery', upload.array('photos', 5), (req, res) => {
if (!req.files || req.files.length === 0) {
return res.status(400).json({ error: 'No files uploaded' });
}
const fileList = req.files.map(file => ({
filename: file.filename,
size: file.size
}));
res.json({ uploaded: fileList.length, files: fileList });
});
Mixed Fields: Files and Text Together
// Accept a mix of named file fields
const uploadFields = upload.fields([
{ name: 'avatar', maxCount: 1 },
{ name: 'resume', maxCount: 1 }
]);
app.post('/upload/profile', uploadFields, (req, res) => {
const avatar = req.files['avatar']?.[0];
const resume = req.files['resume']?.[0];
const { name, bio } = req.body; // Text fields still in req.body
res.json({
name,
bio,
avatarFile: avatar?.filename,
resumeFile: resume?.filename
});
});
Validating File Types and Size
Never trust what the browser claims a file is. Validate the MIME type and size in Multer's configuration:
const imageFilter = (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true); // Accept the file
} else {
cb(new Error('Only JPEG, PNG, and WebP images are allowed'), false);
}
};
const upload = multer({
storage,
fileFilter: imageFilter,
limits: {
fileSize: 5 * 1024 * 1024, // 5 MB maximum
files: 3 // Max 3 files per request
}
});
Handling Multer Errors
Multer throws specific error types. Catch them in your error-handling middleware:
app.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({ error: 'File too large. Maximum size is 5MB.' });
}
if (err.code === 'LIMIT_FILE_COUNT') {
return res.status(400).json({ error: 'Too many files. Maximum is 3.' });
}
return res.status(400).json({ error: err.message });
}
if (err.message.includes('Only JPEG')) {
return res.status(400).json({ error: err.message });
}
next(err);
});
In-Memory Storage (No Disk Write)
For temporary processing — like reading a CSV and importing its rows into a database — use memory storage instead of writing to disk:
const upload = multer({ storage: multer.memoryStorage() });
app.post('/import/csv', upload.single('csvFile'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'No file provided' });
}
// File content available as a Buffer in req.file.buffer
const csvContent = req.file.buffer.toString('utf8');
const rows = csvContent.split('\n');
res.json({ rows: rows.length, preview: rows.slice(0, 3) });
});
Serving Uploaded Files to Users
After saving files to the uploads folder, serve them as static assets so users can view or download them:
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
// Now a file saved as uploads/1714300000000.jpg
// is accessible at: http://localhost:3000/uploads/1714300000000.jpg
File Upload Flow Diagram
User selects file → HTML form submits
│
▼
POST /upload/avatar
Content-Type: multipart/form-data
│
▼
Multer middleware:
┌──────────────────────────────────────────────┐
│ 1. Parse multipart boundary │
│ 2. Run fileFilter → accept or reject │
│ 3. Check file size limit │
│ 4. Generate unique filename │
│ 5. Write file to uploads/ folder │
│ 6. Attach info to req.file │
└────────────────────────┬─────────────────────┘
│
▼
Route handler runs
req.file.filename → save path to database
│
▼
res.json({ filename: '1714300000000.jpg' })
Summary
Multer handles multipart/form-data requests that carry binary file data. Configure multer.diskStorage() to control the save folder and filename, then pass the configured upload middleware to your routes. Use upload.single() for one file, upload.array() for multiple files from one field, and upload.fields() for multiple named file fields. Restrict accepted file types with a fileFilter function and cap file sizes with the limits option. Use multer.memoryStorage() when you need to process file content without saving to disk. Serve saved files with express.static() and handle Multer-specific errors in your error middleware by checking for MulterError instances.
