본문으로 건너뛰기 Multer Complete Guide | File Upload Middleware for Node.js

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:

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.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers Multer, File Upload, Express, Node.js, Backend, Middleware.