Multer Complete Guide | File Upload Middleware for Node.js

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:

  1. Use fileFilter for type validation
  2. Set fileSize limits
  3. Sanitize filenames
  4. Store files outside web root
  5. Process images with Sharp

Next Steps:

  • Process with Sharp
  • Upload to AWS S3
  • Secure with Best Practices

Resources: