Загрузка файлов: Multer

Multer — middleware для Express, обрабатывающий multipart/form-data запросы, что необходимо для загрузки файлов через HTTP-формы или API.

Зачем нужно

Express не умеет парсить multipart/form-data из коробки. express.json и express.urlencoded обрабатывают только JSON и URL-encoded данные. Multer добавляет поддержку загрузки файлов: принимает их в память (memoryStorage) или сохраняет на диск (diskStorage), валидирует тип и размер.

Где используется

  • Загрузка аватаров и изображений профиля
  • Загрузка документов (PDF, Word) в CRM/ERP
  • Импорт данных через CSV/Excel
  • Загрузка медиа (видео, аудио) в медиасервис

Основной контент

Установка

npm install multer

diskStorage — сохранение на диск

const express = require('express');
const multer = require('multer');
const path = require('path');
const crypto = require('crypto');

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, path.join(__dirname, 'uploads')); // папка должна существовать
  },
  filename: (req, file, cb) => {
    // Уникальное имя файла: hash + оригинальное расширение
    const uniqueSuffix = crypto.randomBytes(8).toString('hex');
    const ext = path.extname(file.originalname);
    cb(null, `${uniqueSuffix}${ext}`);
  }
});

const fileFilter = (req, file, cb) => {
  const allowed = ['image/jpeg', 'image/png', 'image/webp'];
  if (allowed.includes(file.mimetype)) {
    cb(null, true);  // принять файл
  } else {
    cb(new Error('Only JPEG, PNG and WebP images are allowed'), false);
  }
};

const upload = multer({
  storage,
  fileFilter,
  limits: {
    fileSize: 5 * 1024 * 1024, // 5 MB
    files: 1
  }
});

Маршруты загрузки

const router = express.Router;

// Один файл — поле 'avatar'
router.post('/avatar',
  upload.single('avatar'),
  (req, res) => {
    // req.file: { fieldname, originalname, encoding, mimetype, filename, path, size }
    if (!req.file) return res.status(400).json({ error: 'File required' });
    res.json({
      filename: req.file.filename,
      url: `/uploads/${req.file.filename}`,
      size: req.file.size
    });
  }
);

// Несколько файлов — поле 'photos', максимум 5
router.post('/gallery',
  upload.array('photos', 5),
  (req, res) => {
    // req.files: массив объектов
    const urls = req.files.map(f => `/uploads/${f.filename}`);
    res.json({ urls });
  }
);

// Несколько полей с разными именами
const cpUpload = upload.fields([
  { name: 'avatar', maxCount: 1 },
  { name: 'documents', maxCount: 5 }
]);

router.post('/profile', cpUpload, (req, res) => {
  const avatar = req.files['avatar']?.[0];
  const docs = req.files['documents'] || ;
  res.json({ avatar: avatar?.filename, docs: docs.map(d => d.filename) });
});

memoryStorage — файл в Buffer

// Для загрузки в S3, CloudStorage без сохранения на диск
const uploadToMemory = multer({
  storage: multer.memoryStorage,
  limits: { fileSize: 10 * 1024 * 1024 }
});

router.post('/upload-s3',
  uploadToMemory.single('file'),
  async (req, res, next) => {
    try {
      // req.file.buffer — Buffer с содержимым файла
      const url = await S3Service.upload(req.file.buffer, req.file.originalname, req.file.mimetype);
      res.json({ url });
    } catch (err) { next(err); }
  }
);

Обработка ошибок Multer

// Multer бросает MulterError при превышении лимитов
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. Max 5MB.' });
    }
    if (err.code === 'LIMIT_FILE_COUNT') {
      return res.status(400).json({ error: 'Too many files.' });
    }
    return res.status(400).json({ error: err.message });
  }
  next(err);
});

Частые ошибки

  • Хранить оригинальное имя файла — пользователи могут загрузить ../../config.js; всегда генерировать уникальное имя через crypto.randomBytes
  • Не валидировать MIME-тип — проверять file.mimetype, а не расширение (расширение можно подделать)
  • Не ограничивать размер — без limits.fileSize можно загрузить произвольно большой файл и заполнить диск
  • Раздавать uploads без ограничений — использовать виртуальный путь /files вместо /uploads, чтобы скрыть структуру директорий

Связанные темы

Ресурсы