Suggest an editImprove this articleRefine the answer for “How to handle file uploads in Express.js with Multer?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Multer** is Express middleware that parses `multipart/form-data` requests and puts uploaded files into `req.file` or `req.files`. ```js const upload = multer({ storage: multer.memoryStorage() }); app.post('/upload', upload.single('avatar'), (req, res) => { res.json({ size: req.file.size }); // req.file.buffer ready for S3 or disk }); ``` **Key:** always add `limits: { fileSize }` and a `fileFilter` callback in production, or any client can send unlimited data to your server.Shown above the full answer for quick recall.Answer (EN)Image**Multer** is Node.js middleware for Express that parses `multipart/form-data` requests, turning raw binary streams into `req.file` or `req.files` objects your route handlers can actually use. ## Theory ### TL;DR - Multer acts like a mailroom clerk: it opens the multipart request, separates form fields from file data, and hands both to your route handler - Express parses JSON and URL-encoded bodies by default but ignores file uploads entirely. Multer fills that gap - `upload.single('field')` for one file, `upload.array('field', n)` for multiple from the same field, `upload.fields([...])` for mixed - `memoryStorage()` keeps files in RAM as a `Buffer`, `diskStorage()` writes to disk. Pick based on what you do next with the file - Always set `limits` and `fileFilter` in production. Without them, anyone can send a 10GB "image" to your server ### Quick example ```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: 'File received', size: req.file.size }); }); app.listen(3000); // POST /upload with form-data field "avatar" = photo.jpg // req.file.buffer is ready for processing or sending to S3 ``` `upload.single('avatar')` is middleware. It runs before your handler, parses the stream, and populates `req.file`. Your handler just reads the result. ### Why Express alone won't work Express parses `application/json` and `application/x-www-form-urlencoded` bodies natively. But file uploads arrive as `multipart/form-data`, a single binary stream split by boundary markers in the `Content-Type` header. Express ignores this stream entirely. Multer hooks into the middleware chain, reads the raw stream with Busboy under the hood, splits it at those boundaries, buffers file chunks into `Buffer` objects, and decodes text fields. After Multer runs, your route has `req.body` for fields and `req.file` for the file. ### Storage options Two built-in options: `multer.memoryStorage()` holds the file in RAM as `req.file.buffer`. Good for processing immediately: resize with `sharp`, upload to S3, run OCR. Bad for large files or high traffic. 100 users uploading 50MB each equals a 5GB RAM spike. `multer.diskStorage({ destination, filename })` writes directly to disk. Good for files you serve statically or process later. You control the path and filename via callbacks. ```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}`); // e.g. 550e8400-e29b-41d4-a716-446655440001.jpg } }); ``` Using `file.originalname` directly as the filename causes collisions. Two users uploading `avatar.jpg` overwrite each other. UUID or `Date.now() + random` fixes this. ### When to use each method - One file from one form field: `upload.single('fieldName')` - Several files from the same field: `upload.array('photos', 5)` (second arg is max count) - Files from different fields: `upload.fields([{name: 'avatar', maxCount: 1}, {name: 'docs', maxCount: 3}])` - No files, just form fields: skip Multer, use `express.urlencoded()` - Process in memory then push to S3: `memoryStorage()` - Serve files from disk: `diskStorage()` ### Validation and limits Without limits, Multer buffers whatever comes in. A `limits` object and a `fileFilter` function cover the two main attack vectors: ```js const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 5 * 1024 * 1024, // 5MB max per file files: 5 // max 5 files per request }, fileFilter: (req, file, cb) => { if (file.mimetype.startsWith('image/')) { cb(null, true); } else { cb(new Error('Only images allowed'), false); } } }); ``` `fileFilter` runs before the file is buffered. Passing `false` rejects the file. Passing an `Error` sends it downstream as an error you catch with error-handling middleware. One thing to know: `mimetype` comes from the client's `Content-Type` header. An attacker can send `image/jpeg` for a PHP file. For real security, check magic bytes in the buffer. JPEG starts with `0xFF 0xD8`, PNG with `0x89 0x50 0x4E 0x47`. Read `req.file.buffer.slice(0, 4)` and compare. ### Error handling Multer throws two kinds of errors: `multer.MulterError` for built-in limits and regular `Error` for custom rejections from `fileFilter`. Handle both in one 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: 'File too large (max 5MB)' }); } return res.status(400).json({ error: err.message }); } if (err.message === 'Only images allowed') { return res.status(400).json({ error: err.message }); } next(err); }); ``` If you use `diskStorage` and a file saves to disk but the request later fails, clean it up with `fs.unlink(req.file.path, () => {})`. ### Common mistakes **Forgetting `enctype="multipart/form-data"` on the HTML form** Without it, the browser sends `application/x-www-form-urlencoded`. Multer skips it, `req.file` is `undefined`, and your handler crashes on `req.file.originalname`. ```html <!-- breaks with no error indication --> <form action="/upload" method="post"> <input type="file" name="avatar"> </form> <!-- correct --> <form action="/upload" method="post" enctype="multipart/form-data"> <input type="file" name="avatar"> </form> ``` **No `limits` in production** A 2GB file sent to an endpoint with `memoryStorage()` and no `fileSize` limit gets buffered entirely in RAM. Node dies. Add `limits: { fileSize: 10 * 1024 * 1024 }` at minimum. **Using `cb(null, false)` in `fileFilter` without throwing** When `fileFilter` calls `cb(null, false)`, the file is rejected quietly. `req.file` is `undefined`. Your handler then crashes reading `req.file.size`. I've seen this burn teams for an hour debugging why uploads disappeared without any error. Throw instead: `cb(new Error('Invalid file type'), false)`. **Storing original filename on disk** `cb(null, file.originalname)` causes collisions. Ten users uploading `photo.jpg` all overwrite the same file. Use `uuidv4()` or `Date.now() + '-' + Math.round(Math.random() * 1e9)`. **Using `memoryStorage` for files you just write to disk anyway** If your handler does `fs.writeFile('uploads/' + file.originalname, req.file.buffer)`, you buffered the file in RAM first, then wrote it. Use `diskStorage` instead. Multer streams it directly to disk and skips the RAM allocation entirely. ### Real-world usage - Profile picture APIs: `memoryStorage()` then `s3.putObject({ Body: req.file.buffer })` - NestJS: `@UseInterceptors(FileInterceptor('file'))` wraps Multer, same behavior underneath - Strapi CMS: uses `diskStorage` for its media library uploads - Image processing pipelines: `memoryStorage()` then `sharp(req.file.buffer).resize(200, 200)` Formidable is an alternative for raw Node.js without Express dependency, with faster stream parsing. Busboy is what Multer uses internally. For maximum throughput when streaming a 1GB file directly to S3 without buffering, drop down to Busboy or `@aws-sdk/lib-storage` directly. ### Follow-up questions **Q:** What is the difference between `upload.single()`, `upload.array()`, and `upload.fields()`? **A:** `single('avatar')` expects one file in the named field and puts it in `req.file`. `array('photos', 5)` takes multiple files from the same field and puts them in `req.files` as an array. `fields([{name:'avatar'}, {name:'docs'}])` handles files from different field names and puts them in `req.files` as an object keyed by field name. **Q:** How do you handle large file uploads without crashing Node? **A:** `memoryStorage` buffers the whole file in RAM. For anything over a few MB at scale, use `diskStorage` so Multer streams directly to disk. For S3, build a stream pipeline with `@aws-sdk/lib-storage` Upload instead of buffering the entire file first. **Q:** How do you validate the actual file type, not just the MIME type? **A:** Check magic bytes in the buffer. JPEG starts with `0xFF 0xD8`, PNG with `0x89 0x50 0x4E 0x47`. Read `req.file.buffer.slice(0, 4)` and compare. The `mimetype` field comes from the client header and can be spoofed freely. **Q:** What happens if a file passes `fileFilter` but disk storage fails mid-write? **A:** Multer may have partially written the file. Check `req.file.path` in your error handler and call `fs.unlink(req.file.path, () => {})` to clean it up. A `try/finally` block in your handler works well here. **Q:** Why does Multer double-buffer when using `memoryStorage` with S3? **A:** Multer collects all incoming chunks into one `Buffer` via Busboy. Then you create a `Readable` stream from that buffer to pass to `s3.putObject`. That is two copies in memory. To avoid it, skip Multer for large S3 uploads and pipe `req` directly through `@aws-sdk/lib-storage` Upload with streaming enabled. ## Examples ### Basic: single file upload with memory storage ```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: 'No file uploaded' }); } // req.file.buffer holds the raw bytes // 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')` runs as middleware before your handler. If the form field name doesn't match `'avatar'`, `req.file` is `undefined`. Always guard with `if (!req.file)`. ### Intermediate: avatar upload with disk storage, validation, and error handling ```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}`); // e.g. 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('Only images allowed'), 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: 'No file' }); 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: 'Max file size is 5MB' }); } if (err.message === 'Only images allowed') { return res.status(400).json({ error: err.message }); } next(err); }); ``` UUID filenames prevent collisions when multiple users upload files with the same name. The error-handling middleware at the end catches both Multer built-in errors and custom ones from `fileFilter`. Without it, `fileFilter` errors surface as unhandled 500s. ### Advanced: file upload directly to S3 from memory ```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('Unsupported format'), 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: 'No file' }); 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 from memoryStorage ContentType: req.file.mimetype })); res.json({ url: `https://${process.env.S3_BUCKET}.s3.amazonaws.com/${key}` }); }); ``` `memoryStorage` is the right choice here because the buffer is needed to send to S3 and the file never touches disk. For files larger than 50MB, switch to `@aws-sdk/lib-storage` Upload with streaming. It handles multipart S3 upload without holding the entire file in RAM.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.