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: