File Uploads with Multer
Reviewed & published by Brayan K
Multer is Express middleware for multipart/form-data that parses incoming file uploads, saving each file to disk or memory and exposing it on req.file or req.files so your route can store and validate it.
Part of the free Node.js course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll learn why uploads need a special parser, how to configure disk vs memory storage, how single, array, and fields differ, and how to lock things down with a fileFilter and size limits — the standard way Express apps accept avatars, photos, and documents.
What You'll Learn in This Lesson
- Why uploads use multipart/form-data and need Multer
- How to wire up multer with diskStorage and a filename function
- The difference between diskStorage and memoryStorage
- single vs array vs fields for one or many files
- How fileFilter rejects unwanted file types
- How limits cap file size and the number of files
1️⃣ Wiring Up Multer
A browser sends an uploaded file as multipart/form-data, not JSON, so express.json() can't touch it. Multer is the middleware that understands that format. You configure a storage engine — here diskStorage with a destination folder and a filename function — and then add upload.single('avatar') to the route.
After Multer runs, the file is on req.file (with filename, originalname, size, mimetype) and any plain text fields are on req.body. The filename function is where you control how files are named on disk — crucial for avoiding collisions.
// Install once: npm install express multer
const express = require('express');
const multer = require('multer');
const app = express();
// diskStorage writes uploaded files straight to a folder on disk.
const storage = multer.diskStorage({
destination: 'uploads/', // where files land
filename: (req, file, cb) => { // what to name them
const unique = Date.now() + '-' + Math.round(Math.random() * 1e9);
cb(null, file.fieldname + '-' + unique);
},
});
const upload = multer({ storage });
// upload.single('avatar') parses ONE file from the form field named "avatar".
// After it runs, the file is on req.file and any text fields are on req.body.
app.post('/profile', upload.single('avatar'), (req, res) => {
res.json({
saved: req.file.filename, // the name on disk
original: req.file.originalname, // what the user called it
size: req.file.size, // bytes
name: req.body.name, // a normal text field from the form
});
});
app.listen(3000);2️⃣ Naming, Filtering & Limits — the Logic
The trickiest parts of uploads aren't the server plumbing — they're the decisions: what to name a file so two uploads don't collide, which types to accept, and how big is too big. The block below runs as plain Node and demonstrates exactly the logic Multer applies for you: a unique filename, a mime-type filter, and a size cap.
const path = require('node:path');
// This is the same filename logic multer's diskStorage.filename runs for you.
function makeFilename(originalname) {
const unique = '1718700000000-517274980'; // Date.now() + random (fixed here)
const ext = path.extname(originalname); // ".png", ".pdf", ...
const base = path.basename(originalname, ext).replace(/\s+/g, '_');
return base + '-' + unique + ext;
}
console.log(makeFilename('My Vacation.png'));
console.log(makeFilename('report final.pdf'));
// This mirrors multer's fileFilter: accept only certain mime types.
function fileFilter(mimetype) {
const allowed = ['image/png', 'image/jpeg'];
return allowed.includes(mimetype);
}
console.log('Accept image/png? ', fileFilter('image/png'));
console.log('Accept application/zip?', fileFilter('application/zip'));
// And this mirrors multer's limits.fileSize check (2 MB cap here).
const MAX = 2 * 1024 * 1024;
function withinLimit(bytes) { return bytes <= MAX; }
console.log('1.5 MB ok?', withinLimit(1.5 * 1024 * 1024));
console.log('5 MB ok? ', withinLimit(5 * 1024 * 1024));3️⃣ Disk vs Memory, and Many Files
Multer ships two storage engines. diskStorage streams the file to a folder (best for keeping files on the server). memoryStorage holds it in RAM as a Buffer on req.file.buffer (best when you forward it straight to cloud storage). The example below uses memory storage and shows the real metadata Multer attaches after a small upload:
const express = require('express');
const multer = require('multer');
const app = express();
// memoryStorage keeps the file in RAM as a Buffer (req.file.buffer) —
// great when you forward it to S3 or process it without touching disk.
const upload = multer({ storage: multer.memoryStorage() });
app.post('/upload', upload.single('avatar'), (req, res) => {
res.json({
field: req.file.fieldname, // "avatar"
name: req.file.originalname, // "hi.txt"
mime: req.file.mimetype, // "text/plain"
bytes: req.file.size, // size in bytes
});
});
// (A test client POSTs a small multipart body to /upload — see the output.)
app.listen(3000);For more than one file, swap the helper: upload.array('photos', 5) for many files under one field, or upload.fields([...]) for several named fields. And the two safety knobs — fileFilter and limits — are configured right on the Multer instance:
const multer = require('multer');
const upload = multer({
storage: multer.diskStorage({ destination: 'uploads/' }),
// Reject anything that isn't an image. Call cb(null, false) to skip the
// file quietly, or pass an Error to fail the whole request.
fileFilter: (req, file, cb) => {
if (file.mimetype.startsWith('image/')) cb(null, true);
else cb(new Error('Only image uploads are allowed'), false);
},
// Hard limits multer enforces for you.
limits: {
fileSize: 2 * 1024 * 1024, // 2 MB per file
files: 5, // at most 5 files
},
});
// single -> req.file (one file)
// array -> req.files (many files, same field name)
// fields -> req.files map (different field names)
const handlers = {
one: upload.single('avatar'),
many: upload.array('photos', 5),
mixed: upload.fields([{ name: 'avatar', maxCount: 1 }, { name: 'docs', maxCount: 3 }]),
};
module.exports = handlers;Your turn. Fill in the two ___ blanks so the function builds a clean stored name, then run it.
const path = require('node:path');
// 🎯 YOUR TURN — replace each ___ then run it.
// Build a safe stored filename: <basename>-<id><ext>, e.g. "logo-7.png".
function storedName(originalname, id) {
const ext = path.extname(originalname); // ".png"
const base = path.basename(originalname, ___); // 👉 ext (strips ".png")
return base + '-' + id + ___; // 👉 ext (re-adds ".png")
}
console.log(storedName('logo.png', 7));
console.log(storedName('resume.pdf', 12));
// ✅ Expected output:
// logo-7.png
// resume-12.pdfMini-Challenge: An Upload Gatekeeper
No blanks this time — just a brief and an outline. Write the function yourself, run it, and check your output against the example. This is the same decision Multer's fileFilter and limits make.
// 🎯 MINI-CHALLENGE: a tiny upload gatekeeper (no server needed).
// Write checkUpload(file) where file = { mimetype, size }.
// Rules:
// - allow only 'image/png' and 'image/jpeg'
// - max size is 1 MB (1024 * 1024 bytes)
// - return 'accepted', 'bad type', or 'too big'
// Then test it with the three calls below.
// your code here
// console.log(checkUpload({ mimetype: 'image/png', size: 500000 }));
// console.log(checkUpload({ mimetype: 'application/zip', size: 1000 }));
// console.log(checkUpload({ mimetype: 'image/jpeg', size: 5000000 }));
//
// ✅ Example output:
// accepted
// bad type
// too bigCommon Errors (and the fix)
- req.file is undefined — The form field name doesn't match the name you passed to upload.single('avatar'), or the form is missing enctype="multipart/form-data". They must agree.
- "MulterError: File too large" — A file exceeded limits.fileSize. That's the limit working — return a friendly 413 to the client instead of crashing.
- "MulterError: Unexpected field" — A file arrived under a field name you didn't configure (e.g. you used single('avatar') but the form sent photo). Match the names or use fields.
- "ENOENT" when saving — The destination folder doesn't exist. Multer won't create it; make uploads/ first (e.g. fs.mkdirSync('uploads', { recursive: true })).
- Files overwrite each other — You returned a fixed filename. Add a timestamp or random id so every upload is unique.
Pro Tips
- 💡 Always set limits.fileSize: without it, a single huge upload can exhaust memory or disk. Pick a sane cap per route.
- 💡 Generate your own filenames: never write a user-supplied name straight to disk — borrow only the extension.
- 💡 Memory storage for the cloud: if the file goes to S3 or a resizer, skip the disk entirely with memoryStorage and a tight size limit.
📋 Quick Reference — Multer
| Task | Syntax |
|---|---|
| One file | upload.single('avatar') |
| Many files | upload.array('photos', 5) |
| Named fields | upload.fields([{ name, maxCount }]) |
| Save to disk | multer.diskStorage({ destination, filename }) |
| Keep in RAM | multer.memoryStorage() |
| Cap size | { limits: { fileSize } } |
| Filter types | { fileFilter: (req, file, cb) => ... } |
🎉 Lesson Complete!
- ✅ Uploads arrive as multipart/form-data, which only Multer can parse
- ✅ diskStorage saves to a folder; memoryStorage keeps a Buffer in RAM
- ✅ single, array, and fields handle one or many files
- ✅ The filename function lets you generate safe, unique names
- ✅ fileFilter rejects unwanted types; limits caps size and count
- ✅ Never trust the original filename — borrow only the extension
Practice quiz
Why can't express.json() parse a file upload?
- Files are too large for it
- Uploads are sent as multipart/form-data, a different encoding it doesn't understand
- It only runs on GET requests
- It needs a database first
Answer: Uploads are sent as multipart/form-data, a different encoding it doesn't understand. express.json() only parses application/json; uploads arrive as multipart/form-data, which needs Multer.
After upload.single('avatar') runs, where is the uploaded file?
- req.body.avatar
- req.params.file
- req.file
- req.upload
Answer: req.file. upload.single puts the single file on req.file and any text fields on req.body.
Which storage engine keeps the file in RAM as a Buffer on req.file.buffer?
- multer.memoryStorage()
- multer.diskStorage()
- multer.bufferStorage()
- multer.ramStorage()
Answer: multer.memoryStorage(). memoryStorage holds the file in RAM as a Buffer, ideal when forwarding it straight to cloud storage.
Which helper accepts up to five files all sharing one field name?
- upload.single('photos')
- upload.fields('photos', 5)
- upload.many('photos')
- upload.array('photos', 5)
Answer: upload.array('photos', 5). upload.array('photos', 5) accepts many files under one field name and puts them on req.files.
What does the limits.fileSize option do?
- Sets the upload folder
- Caps the bytes per file, aborting with a MulterError if exceeded
- Renames the file
- Counts the number of fields
Answer: Caps the bytes per file, aborting with a MulterError if exceeded. limits.fileSize caps each file's size; Multer aborts with a MulterError when a file is too large.
Inside a fileFilter, how do you reject a file and fail the whole request?
- return false
- cb(null, true)
- cb(new Error('not allowed'), false)
- throw 'no'
Answer: cb(new Error('not allowed'), false). Pass an Error to cb to fail the request; cb(null, false) skips the file quietly.
Why should you never write the user's original filename straight to disk?
- It is always too long
- Multer forbids it
- It is slower than a random name
- A user could supply a path like ../../etc/passwd, so generate your own name and borrow only the extension
Answer: A user could supply a path like ../../etc/passwd, so generate your own name and borrow only the extension. The original name is untrusted; generate a safe unique name and keep only the extension.
req.file is undefined after an upload. What is a likely cause?
- The form field name doesn't match the name in upload.single(), or the form lacks multipart/form-data
- The file was too small
- memoryStorage was used
- The server is on the wrong port
Answer: The form field name doesn't match the name in upload.single(), or the form lacks multipart/form-data. The form field name must match upload.single's name and the form must use enctype multipart/form-data.
Why is trusting file.mimetype alone not enough for security?
- It is always empty
- It is supplied by the client and can be spoofed
- It is only set for images
- Multer deletes it
Answer: It is supplied by the client and can be spoofed. mimetype comes from the client and can be faked, so also validate real contents server-side.
Which upload helper accepts files from several differently-named fields at once?
- upload.fields([{ name: 'avatar', maxCount: 1 }, { name: 'docs', maxCount: 3 }])
- upload.single('all')
- upload.array('fields')
- upload.any('avatar', 'docs')
Answer: upload.fields([{ name: 'avatar', maxCount: 1 }, { name: 'docs', maxCount: 3 }]). upload.fields([...]) accepts several named fields and keys them on req.files by field name.
Continue this course
- Previous: JWT Authentication in Express
- Next: Logging with morgan & pino — Add HTTP request logging and structured JSON logs with levels
- Quick reference: Node.js cheat sheet
Frequently asked questions
Why can't express.json() parse a file upload?
express.json() only understands an application/json body. File uploads from an HTML form are sent as multipart/form-data, a completely different encoding that interleaves file bytes and text fields with boundary markers. You need a parser that understands that format — that's exactly what Multer is. Add Multer's middleware (upload.single, upload.array, etc.) to the route that receives the upload, and it will populate req.file / req.files and put any text fields on req.body.
Should I use diskStorage or memoryStorage?
Use diskStorage when you want the file saved to a folder on the server and you're happy serving it from there or moving it later — it streams to disk so it handles large files without eating memory. Use memoryStorage when you immediately forward the file somewhere else (an S3 bucket, an image resizer, a virus scanner) and never want it on the local disk; the file arrives as a Buffer on req.file.buffer. Memory storage is convenient but holds the whole file in RAM, so always pair it with a sensible fileSize limit.
What's the difference between upload.single, upload.array, and upload.fields?
They differ by how many files and how many field names you accept. upload.single('avatar') accepts exactly one file from the field named 'avatar' and puts it on req.file. upload.array('photos', 5) accepts up to five files all sharing the field name 'photos' and puts them on req.files as an array. upload.fields([...]) accepts files from several differently-named fields at once (for example one 'avatar' plus up to three 'docs') and puts them on req.files keyed by field name. Pick the one that matches your form's input fields.
How do I stop people from uploading huge or dangerous files?
Combine two of Multer's options. limits: { fileSize, files } caps the bytes per file and the number of files, and Multer aborts with a MulterError if a request exceeds them. fileFilter lets you inspect file.mimetype (and originalname) and call cb(null, false) to skip a file or cb(new Error(...)) to reject the request — for example allowing only image/* types. Note that mimetype is supplied by the client and can be spoofed, so for true safety also validate the file's real contents server-side and store uploads outside any folder that gets executed.