Multer Complete Guide | File Upload Middleware for Node.js
이 글의 핵심
Multer is a Node.js middleware for handling multipart/form-data, primarily used for file uploads. It's built on top of busboy for maximum efficiency.
Introduction
Multer is a Node.js middleware for handling multipart/form-data, which is primarily used for uploading files. It’s built on busboy for high efficiency.
Why Multer?
Without Multer (manual parsing is painful):
// Complex manual parsing of multipart data
// Dealing with streams, boundaries, encoding...
With Multer:
const multer = require('multer');
const upload = multer({ dest: 'uploads/' });
app.post('/upload', upload.single('file'), (req, res) => {
console.log(req.file); // File info
res.send('File uploaded!');
});
1. Installation
npm install multer
2. Basic Usage
Single File Upload
const express = require('express');
const multer = require('multer');
const app = express();
const upload = multer({ dest: 'uploads/' });
app.post('/upload', upload.single('avatar'), (req, res) => {
// req.file is the `avatar` file
// req.body will hold text fields
console.log(req.file);
console.log(req.body);
res.json({ file: req.file });
});
Multiple Files
// Multiple files with same field name
app.post('/photos', upload.array('photos', 12), (req, res) => {
// req.files is array of `photos` files (max 12)
console.log(req.files);
res.json({ files: req.files });
});
// Multiple files with different field names
app.post('/profile', upload.fields([
{ name: 'avatar', maxCount: 1 },
{ name: 'gallery', maxCount: 8 }
]), (req, res) => {
// req.files is an object with keys 'avatar' and 'gallery'
console.log(req.files.avatar);
console.log(req.files.gallery);
res.json({ files: req.files });
});
3. Storage Engine
Disk Storage
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/');
},
filename: function (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 });
Memory Storage
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });
app.post('/upload', upload.single('file'), (req, res) => {
// req.file.buffer contains the file in memory
console.log(req.file.buffer);
});
4. File Information
app.post('/upload', upload.single('file'), (req, res) => {
console.log(req.file);
// {
// fieldname: 'file',
// originalname: 'photo.jpg',
// encoding: '7bit',
// mimetype: 'image/jpeg',
// destination: 'uploads/',
// filename: 'file-1234567890.jpg',
// path: 'uploads/file-1234567890.jpg',
// size: 12345
// }
});
5. File Filtering
const fileFilter = (req, file, cb) => {
// Accept images only
if (file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('Only images are allowed!'), false);
}
};
const upload = multer({
dest: 'uploads/',
fileFilter: fileFilter,
limits: {
fileSize: 5 * 1024 * 1024, // 5MB
}
});
// Specific file types
const imageFilter = (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Invalid file type. Only JPEG, PNG and GIF are allowed.'));
}
};
6. Limits Configuration
const upload = multer({
dest: 'uploads/',
limits: {
fileSize: 5 * 1024 * 1024, // 5MB
files: 10, // Max 10 files
fields: 20, // Max 20 non-file fields
fieldNameSize: 100, // Max field name size
fieldSize: 1024 * 1024, // Max field value size (1MB)
}
});
7. Error Handling
app.post('/upload', upload.single('file'), (req, res) => {
res.json({ file: req.file });
});
// Error handling middleware
app.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
// Multer error
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({ error: 'File too large' });
}
if (err.code === 'LIMIT_FILE_COUNT') {
return res.status(400).json({ error: 'Too many files' });
}
if (err.code === 'LIMIT_UNEXPECTED_FILE') {
return res.status(400).json({ error: 'Unexpected field' });
}
} else if (err) {
// Custom error
return res.status(400).json({ error: err.message });
}
next();
});
8. Image Processing with Sharp
npm install sharp
const sharp = require('sharp');
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });
app.post('/upload', upload.single('image'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
// Resize and optimize
const filename = `${Date.now()}-optimized.jpg`;
await sharp(req.file.buffer)
.resize(800, 600, { fit: 'inside' })
.jpeg({ quality: 80 })
.toFile(`uploads/${filename}`);
res.json({
message: 'Image uploaded and optimized',
filename: filename
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
9. Multiple Sizes (Thumbnails)
app.post('/upload', upload.single('image'), async (req, res) => {
try {
const filename = `${Date.now()}`;
const buffer = req.file.buffer;
// Original
await sharp(buffer)
.jpeg({ quality: 90 })
.toFile(`uploads/${filename}-original.jpg`);
// Large
await sharp(buffer)
.resize(1200, 1200, { fit: 'inside' })
.jpeg({ quality: 85 })
.toFile(`uploads/${filename}-large.jpg`);
// Thumbnail
await sharp(buffer)
.resize(300, 300, { fit: 'cover' })
.jpeg({ quality: 80 })
.toFile(`uploads/${filename}-thumb.jpg`);
res.json({
original: `${filename}-original.jpg`,
large: `${filename}-large.jpg`,
thumbnail: `${filename}-thumb.jpg`,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
10. AWS S3 Upload
npm install multer-s3 @aws-sdk/client-s3
const multer = require('multer');
const multerS3 = require('multer-s3');
const { S3Client } = require('@aws-sdk/client-s3');
const s3 = new S3Client({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
});
const upload = multer({
storage: multerS3({
s3: s3,
bucket: 'my-bucket',
acl: 'public-read',
metadata: function (req, file, cb) {
cb(null, { fieldName: file.fieldname });
},
key: function (req, file, cb) {
const filename = `${Date.now()}-${file.originalname}`;
cb(null, filename);
}
})
});
app.post('/upload', upload.single('file'), (req, res) => {
res.json({
message: 'File uploaded to S3',
url: req.file.location,
key: req.file.key
});
});
11. Progress Tracking
const busboy = require('busboy');
app.post('/upload-with-progress', (req, res) => {
const bb = busboy({ headers: req.headers });
bb.on('file', (fieldname, file, filename, encoding, mimetype) => {
let fileSize = 0;
const saveTo = `uploads/${Date.now()}-${filename}`;
const writeStream = fs.createWriteStream(saveTo);
file.on('data', (data) => {
fileSize += data.length;
// Send progress via WebSocket or SSE
console.log(`Uploaded: ${fileSize} bytes`);
});
file.pipe(writeStream);
file.on('end', () => {
console.log(`File ${filename} finished uploading`);
});
});
bb.on('finish', () => {
res.json({ message: 'Upload complete' });
});
req.pipe(bb);
});
12. Security Best Practices
1. Validate File Type
const fileFilter = (req, file, cb) => {
// Check MIME type
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!allowedTypes.includes(file.mimetype)) {
return cb(new Error('Invalid file type'), false);
}
// Check file extension
const ext = path.extname(file.originalname).toLowerCase();
const allowedExts = ['.jpg', '.jpeg', '.png', '.gif'];
if (!allowedExts.includes(ext)) {
return cb(new Error('Invalid file extension'), false);
}
cb(null, true);
};
2. Limit File Size
const upload = multer({
dest: 'uploads/',
limits: {
fileSize: 5 * 1024 * 1024, // 5MB
}
});
3. Sanitize Filenames
const storage = multer.diskStorage({
filename: function (req, file, cb) {
// Remove special characters
const safeName = file.originalname
.replace(/[^a-zA-Z0-9.-]/g, '_')
.toLowerCase();
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, uniqueSuffix + '-' + safeName);
}
});
4. Store Outside Web Root
// Bad: uploads/ is publicly accessible
const upload = multer({ dest: 'public/uploads/' });
// Good: uploads/ is outside public directory
const upload = multer({ dest: 'private/uploads/' });
// Serve files with authentication
app.get('/files/:filename', authenticate, (req, res) => {
const filepath = path.join(__dirname, 'private/uploads', req.params.filename);
res.sendFile(filepath);
});
5. Scan for Malware
const { exec } = require('child_process');
async function scanFile(filepath) {
return new Promise((resolve, reject) => {
exec(`clamscan ${filepath}`, (error, stdout) => {
if (stdout.includes('OK')) {
resolve(true);
} else {
reject(new Error('Malware detected'));
}
});
});
}
app.post('/upload', upload.single('file'), async (req, res) => {
try {
await scanFile(req.file.path);
res.json({ message: 'File is safe' });
} catch (error) {
fs.unlinkSync(req.file.path); // Delete infected file
res.status(400).json({ error: 'Malware detected' });
}
});
13. Real-World Example: Avatar Upload
const express = require('express');
const multer = require('multer');
const sharp = require('sharp');
const path = require('path');
const fs = require('fs').promises;
const app = express();
// Configure storage
const storage = multer.memoryStorage();
// File filter
const fileFilter = (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Only JPEG, PNG, and GIF images are allowed'));
}
};
// Configure multer
const upload = multer({
storage: storage,
fileFilter: fileFilter,
limits: {
fileSize: 5 * 1024 * 1024, // 5MB
}
});
// Upload endpoint
app.post('/api/users/:userId/avatar',
authenticate, // Your auth middleware
upload.single('avatar'),
async (req, res) => {
try {
const userId = req.params.userId;
// Check authorization
if (req.user.id !== userId) {
return res.status(403).json({ error: 'Unauthorized' });
}
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
// Delete old avatar if exists
const user = await db.users.findById(userId);
if (user.avatar) {
await fs.unlink(`uploads/avatars/${user.avatar}`).catch(() => {});
}
// Process image
const filename = `${userId}-${Date.now()}.jpg`;
await sharp(req.file.buffer)
.resize(400, 400, { fit: 'cover' })
.jpeg({ quality: 90 })
.toFile(`uploads/avatars/${filename}`);
// Update database
await db.users.update(userId, { avatar: filename });
res.json({
message: 'Avatar uploaded successfully',
avatar: filename,
url: `/uploads/avatars/${filename}`
});
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Upload failed' });
}
}
);
// Error handling
app.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
return res.status(400).json({ error: err.message });
}
next(err);
});
Summary
Multer simplifies file uploads in Express:
- Easy integration with Express middleware
- Multiple storage options (disk, memory, S3)
- File filtering and validation
- Size limits for security
- Works with image processing libraries
Key Takeaways:
- Use fileFilter for type validation
- Set fileSize limits
- Sanitize filenames
- Store files outside web root
- Process images with Sharp
Next Steps:
- Process with Sharp
- Upload to AWS S3
- Secure with Best Practices
Resources:
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. Complete Multer guide for handling file uploads in Express. Learn single/multiple files, storage engines, validation, im…
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- Winston Complete Guide | Node.js Logging Library
- C++ JSON Parsing: nlohmann/json, RapidJSON, Custom Types,
- Express.js Complete Guide | Fast Node.js Web Framework
Keywords Covered in This Article (Related Search Terms)
This article covers Multer, File Upload, Express, Node.js, Backend, Middleware.