Skip to main content

Як обробляти завантаження файлів в Express.js за допомогою Multer?

Multer - це middleware для Express, яке парсить запити multipart/form-data і перетворює сирі бінарні потоки на об'єкти req.file або req.files, з якими можуть працювати обробники маршрутів.

Теорія

TL;DR

  • Multer як поштовий клерк: відкриває multipart-запит, відокремлює поля форми від файлів, передає обидва в обробник маршруту
  • Express за замовчуванням парсить JSON та URL-encoded тіла, але файлові завантаження ігнорує повністю. Multer закриває цю прогалину
  • upload.single('field') для одного файлу, upload.array('field', n) для кількох з одного поля, upload.fields([...]) для змішаних
  • memoryStorage() тримає файл у RAM як Buffer, diskStorage() записує на диск. Вибір залежить від того, що робитимеш з файлом далі
  • Завжди встановлюй limits та fileFilter у продакшені. Без них будь-хто може надіслати 10GB «зображення» на твій сервер

Швидкий приклад

js
const express = require('express'); const multer = require('multer'); const app = express(); const upload = multer({ storage: multer.memoryStorage() }); app.post('/upload', upload.single('avatar'), (req, res) => { // req.file = { fieldname, originalname, mimetype, size, buffer } res.json({ message: 'Файл отримано', size: req.file.size }); }); app.listen(3000); // POST /upload з form-data полем "avatar" = photo.jpg // req.file.buffer готовий до обробки або відправки на S3

upload.single('avatar') - це middleware. Він спрацьовує до твого обробника, парсить потік і заповнює req.file. Обробник просто читає результат.

Чому Express один не справиться

Express нативно парсить тіла application/json та application/x-www-form-urlencoded. Але файлові завантаження приходять як multipart/form-data - єдиний бінарний потік, розбитий boundary-маркерами з заголовка Content-Type. Express цей потік просто ігнорує.

Multer підключається до ланцюжка middleware, читає сирий потік через Busboy, ділить його по boundary-маркерах, буферизує файлові чанки в об'єкти Buffer і декодує текстові поля. Після Multer твій маршрут має req.body для полів і req.file для файлу.

Опції зберігання

Два вбудованих варіанти:

multer.memoryStorage() тримає файл у RAM як req.file.buffer. Підходить для обробки одразу після отримання: resize через sharp, відправка на S3, OCR. Не підходить для великих файлів або великого навантаження. 100 користувачів по 50MB кожен - це стрибок RAM на 5GB.

multer.diskStorage({ destination, filename }) записує прямо на диск. Підходить для файлів, які треба роздавати статично або обробити пізніше. Шлях і ім'я файлу контролюєш через callback-функції.

js
const path = require('path'); const { v4: uuidv4 } = require('uuid'); const storage = multer.diskStorage({ destination: (req, file, cb) => cb(null, 'public/uploads/'), filename: (req, file, cb) => { const ext = path.extname(file.originalname); cb(null, `${uuidv4()}${ext}`); // напр. 550e8400-e29b-41d4-a716-446655440001.jpg } });

Якщо використовувати file.originalname напряму як ім'я файлу, виникнуть колізії. Двоє користувачів з файлом avatar.jpg перезапишуть один одного. UUID або Date.now() + random вирішують цю проблему.

Коли що використовувати

  • Один файл з одного поля форми: upload.single('fieldName')
  • Кілька файлів з одного поля: upload.array('photos', 5) (другий аргумент - максимальна кількість)
  • Файли з різних полів: upload.fields([{name: 'avatar', maxCount: 1}, {name: 'docs', maxCount: 3}])
  • Без файлів, тільки поля форми: Multer не потрібен, використовуй express.urlencoded()
  • Обробка в пам'яті та відправка на S3: memoryStorage()
  • Роздача файлів з диску: diskStorage()

Валідація та обмеження

Без обмежень Multer буферизує все, що приходить. Об'єкт limits і функція fileFilter закривають два основних вектори атак:

js
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 5 * 1024 * 1024, // макс 5MB на файл files: 5 // макс 5 файлів на запит }, fileFilter: (req, file, cb) => { if (file.mimetype.startsWith('image/')) { cb(null, true); } else { cb(new Error('Дозволені тільки зображення'), false); } } });

fileFilter викликається до буферизації файлу. cb(null, false) відхиляє файл. cb(new Error(...)) передає помилку далі по ланцюжку middleware.

Важливий момент: mimetype береться з заголовка Content-Type клієнта. Зловмисник може надіслати image/jpeg для PHP-файлу. Для реального захисту перевіряй magic bytes у буфері. JPEG починається з 0xFF 0xD8, PNG з 0x89 0x50 0x4E 0x47. Читай req.file.buffer.slice(0, 4) і порівнюй.

Обробка помилок

Multer кидає два типи помилок: multer.MulterError при перевищенні вбудованих лімітів і звичайний Error з fileFilter або твого коду. Обробляй обидва в одному middleware:

js
app.use((err, req, res, next) => { if (err instanceof multer.MulterError) { if (err.code === 'LIMIT_FILE_SIZE') { return res.status(400).json({ error: 'Файл занадто великий (макс 5MB)' }); } return res.status(400).json({ error: err.message }); } if (err.message === 'Дозволені тільки зображення') { return res.status(400).json({ error: err.message }); } next(err); });

Якщо використовуєш diskStorage і файл зберігся на диск, але запит пізніше завалився - прибери файл: fs.unlink(req.file.path, () => {}).

Типові помилки

Забутий enctype="multipart/form-data" в HTML-формі

Без нього браузер відправляє application/x-www-form-urlencoded. Multer пропускає такий запит, req.file дорівнює undefined, і обробник падає на req.file.originalname.

html
<!-- ламається без жодних натяків на причину --> <form action="/upload" method="post"> <input type="file" name="avatar"> </form> <!-- правильно --> <form action="/upload" method="post" enctype="multipart/form-data"> <input type="file" name="avatar"> </form>

Відсутність limits у продакшені

Надсилання 2GB файлу на endpoint з memoryStorage() і без ліміту fileSize буферизує все в RAM. Node падає. Додавай limits: { fileSize: 10 * 1024 * 1024 } як мінімум.

cb(null, false) у fileFilter без виключення

Коли fileFilter викликає cb(null, false), файл відхиляється тихо. req.file дорівнює undefined. Обробник падає при спробі прочитати req.file.size. Я бачив, як команди витрачали годину на дебаг того, чому завантаження зникають без помилок. Кидай виключення замість цього: cb(new Error('Невірний тип файлу'), false).

Оригінальне ім'я файлу на диску

cb(null, file.originalname) призводить до колізій. Десятеро користувачів з файлом photo.jpg перепишуть один і той самий файл. Використовуй uuidv4() або Date.now() + '-' + Math.round(Math.random() * 1e9).

memoryStorage для файлів, які все одно йдуть на диск

Якщо обробник робить fs.writeFile('uploads/' + file.originalname, req.file.buffer), файл спочатку буферизується в RAM і лише потім записується на диск. Використовуй diskStorage - Multer стримить його напряму і RAM не витрачається.

Де зустрічається у реальних проєктах

  • API профілів: memoryStorage(), потім s3.putObject({ Body: req.file.buffer })
  • NestJS: @UseInterceptors(FileInterceptor('file')) обгортає Multer, поведінка та сама
  • Strapi CMS: використовує diskStorage для медіабібліотеки
  • Пайплайни обробки зображень: memoryStorage(), потім sharp(req.file.buffer).resize(200, 200)

Formidable - альтернатива для чистого Node.js без залежності від Express, з швидшим парсингом потоків. Busboy - це те, що Multer використовує під капотом. Якщо потрібна максимальна продуктивність при стримінгу гігабайтного файлу на S3 без буферизації, дивись у бік @aws-sdk/lib-storage або Busboy напряму.

Питання на співбесіді

Q: У чому різниця між upload.single(), upload.array() і upload.fields()?
A: single('avatar') очікує один файл у названому полі, кладе його в req.file. array('photos', 5) приймає кілька файлів з одного поля, кладе в масив req.files. fields([{name:'avatar'}, {name:'docs'}]) обробляє файли з різних полів, req.files стає об'єктом з ключами за іменами полів.

Q: Як обробляти великі файли без краша Node?
A: memoryStorage буферизує весь файл у RAM. Для великих файлів при навантаженні використовуй diskStorage - Multer стримить прямо на диск. Для S3 будуй stream pipeline через @aws-sdk/lib-storage Upload замість буферизації цілого файлу.

Q: Як перевірити реальний тип файлу, а не тільки MIME?
A: Перевіряй magic bytes у буфері. JPEG починається з 0xFF 0xD8, PNG з 0x89 0x50 0x4E 0x47. Читай req.file.buffer.slice(0, 4) і порівнюй. Поле mimetype береться з заголовка клієнта і може бути підроблене.

Q: Що станеться, якщо файл пройшов fileFilter, але запис на диск провалився?
A: Multer міг частково записати файл. Перевір req.file.path в обробнику помилок і виклич fs.unlink(req.file.path, () => {}) для прибирання. Конструкція try/finally в обробнику теж підходить.

Q: Чому Multer подвійно буферизує файли з memoryStorage при відправці на S3?
A: Multer збирає всі вхідні чанки в один Buffer через Busboy. Потім ти створюєш Readable stream з цього буфера для s3.putObject. Два екземпляри в пам'яті одночасно. Щоб уникнути цього, для великих S3-завантажень пропускай Multer і передавай req напряму через @aws-sdk/lib-storage Upload зі стримінгом.

Приклади

Базовий: завантаження одного файлу в пам'ять

js
const express = require('express'); const multer = require('multer'); const app = express(); const upload = multer({ storage: multer.memoryStorage() }); app.post('/api/avatar', upload.single('avatar'), (req, res) => { if (!req.file) { return res.status(400).json({ error: 'Файл не надіслано' }); } // req.file.buffer містить сирі байти // req.file.mimetype = 'image/jpeg' // req.file.size = 45678 res.json({ name: req.file.originalname, size: req.file.size, type: req.file.mimetype }); }); app.listen(3000); // curl -F "avatar=@photo.jpg" http://localhost:3000/api/avatar

upload.single('avatar') виконується як middleware до обробника. Якщо ім'я поля форми не збігається з 'avatar', req.file дорівнює undefined. Завжди перевіряй через if (!req.file).

Середній: завантаження аватара на диск з валідацією та обробкою помилок

js
const path = require('path'); const { v4: uuidv4 } = require('uuid'); const multer = require('multer'); const storage = multer.diskStorage({ destination: (req, file, cb) => cb(null, 'public/uploads/avatars/'), filename: (req, file, cb) => { const ext = path.extname(file.originalname); cb(null, `avatar-${uuidv4()}${ext}`); // напр. avatar-550e8400-e29b-41d4-a716-446655440001.jpg } }); const upload = multer({ storage, fileFilter: (req, file, cb) => { if (file.mimetype.startsWith('image/')) { cb(null, true); } else { cb(new Error('Дозволені тільки зображення'), false); } }, limits: { fileSize: 5 * 1024 * 1024 } // 5MB }); app.post('/api/users/avatar', upload.single('avatar'), (req, res) => { if (!req.file) return res.status(400).json({ error: 'Файл не надіслано' }); res.json({ avatarUrl: `/uploads/avatars/${req.file.filename}` }); }); app.use((err, req, res, next) => { if (err instanceof multer.MulterError && err.code === 'LIMIT_FILE_SIZE') { return res.status(400).json({ error: 'Максимальний розмір файлу 5MB' }); } if (err.message === 'Дозволені тільки зображення') { return res.status(400).json({ error: err.message }); } next(err); });

UUID-імена файлів запобігають колізіям при однакових іменах від різних користувачів. Middleware обробки помилок наприкінці перехоплює і вбудовані помилки Multer, і кастомні з fileFilter. Без нього помилки fileFilter стають необробленими 500-ми.

Просунутий: завантаження файлу на S3 через буфер

js
const multer = require('multer'); const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3'); const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 }, // 10MB fileFilter: (req, file, cb) => { const allowed = ['image/jpeg', 'image/png', 'image/webp']; if (allowed.includes(file.mimetype)) cb(null, true); else cb(new Error('Непідтримуваний формат'), false); } }); const s3 = new S3Client({ region: 'us-east-1' }); app.post('/api/photos', upload.single('photo'), async (req, res) => { if (!req.file) return res.status(400).json({ error: 'Файл не надіслано' }); const key = `photos/${Date.now()}-${req.file.originalname}`; await s3.send(new PutObjectCommand({ Bucket: process.env.S3_BUCKET, Key: key, Body: req.file.buffer, // buffer з memoryStorage ContentType: req.file.mimetype })); res.json({ url: `https://${process.env.S3_BUCKET}.s3.amazonaws.com/${key}` }); });

memoryStorage тут правильний вибір: буфер потрібен для відправки на S3, файл ніколи не торкається диску. Для файлів більше 50MB розглянь @aws-sdk/lib-storage Upload зі стримінгом - він робить multipart S3-завантаження без буферизації всього файлу одразу.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?