Episode 3 — NodeJS MongoDB Backend Architecture / 3.7 — Handling Files with Express
3.7.a — Understanding Multer
In one sentence: Multer is the de-facto Express middleware for handling
multipart/form-data— the encoding type required for file uploads — because Express's built-in body parsers (express.json(),express.urlencoded()) cannot process binary file data.
Table of Contents
- 1. Why Standard Body Parsers Cannot Handle Files
- 2. What Is multipart/form-data?
- 3. What Is Multer?
- 4. Installing Multer
- 5. How Multer Works Internally
- 6. Multer Configuration Object
- 7. Upload Methods — single, array, fields, none
- 8. File Size Limits and File Type Filtering
- 9. Key Takeaways
1. Why Standard Body Parsers Cannot Handle Files
Express ships with two body parsers:
app.use(express.json()); // parses JSON bodies (Content-Type: application/json)
app.use(express.urlencoded()); // parses URL-encoded bodies (Content-Type: application/x-www-form-urlencoded)
Neither can handle files because:
| Parser | Content-Type It Handles | Can Handle Files? |
|---|---|---|
express.json() | application/json | No |
express.urlencoded() | application/x-www-form-urlencoded | No |
| Multer | multipart/form-data | Yes |
┌─────────────────────────────────────────────────────────────────┐
│ POST /api/users │
│ Content-Type: application/json │
│ Body: { "name": "Alice" } │
│ │
│ → express.json() can handle this │
│ → req.body = { name: "Alice" } │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ POST /api/upload │
│ Content-Type: multipart/form-data; boundary=----abc123 │
│ Body: binary file data + text fields mixed together │
│ │
│ → express.json() IGNORES this (wrong Content-Type) │
│ → express.urlencoded() IGNORES this │
│ → Multer HANDLES this perfectly │
└─────────────────────────────────────────────────────────────────┘
Key point: If you try to access
req.bodyorreq.fileon a multipart request without Multer, you getundefined. This is the #1 beginner mistake with file uploads.
2. What Is multipart/form-data?
When an HTML form includes a file input, the browser must use a special encoding called multipart/form-data. This encoding:
- Splits the body into parts separated by a boundary string
- Each part can be text OR binary data
- Each part has its own headers (Content-Disposition, Content-Type)
HTML form that triggers multipart encoding
<!-- The enctype attribute is REQUIRED for file uploads -->
<form action="/upload" method="POST" enctype="multipart/form-data">
<input type="text" name="username" />
<input type="file" name="avatar" />
<button type="submit">Upload</button>
</form>
What the browser actually sends
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxk
------WebKitFormBoundary7MA4YWxk
Content-Disposition: form-data; name="username"
alice
------WebKitFormBoundary7MA4YWxk
Content-Disposition: form-data; name="avatar"; filename="profile.jpg"
Content-Type: image/jpeg
[... raw binary image data ...]
------WebKitFormBoundary7MA4YWxk--
Three encoding types compared
| Encoding | enctype Value | Use Case |
|---|---|---|
| URL-encoded | application/x-www-form-urlencoded | Text-only forms (login, search) |
| Multipart | multipart/form-data | Forms with file uploads |
| JSON | application/json | JavaScript API calls (fetch/axios) |
Rule of thumb: If your form has
<input type="file">, you MUST setenctype="multipart/form-data". Forgetting this is the second most common file upload mistake.
3. What Is Multer?
Multer is a Node.js middleware for handling multipart/form-data. It is built on top of busboy, a streaming multipart parser.
┌──────────────────────────────────────────────────────────────┐
│ MULTER'S JOB │
│ │
│ Incoming multipart request │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ MULTER │ │
│ │ │ │
│ │ 1. Parse boundary & parts │ │
│ │ 2. Extract text fields → req.body │
│ │ 3. Extract files → req.file or req.files │
│ │ 4. Apply file filter (accept/reject) │
│ │ 5. Apply size limits │
│ │ 6. Write to disk OR hold in memory │
│ └─────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Route handler receives: │
│ req.body → { username: "alice" } │
│ req.file → { fieldname, originalname, mimetype, ... } │
└──────────────────────────────────────────────────────────────┘
Key facts about Multer
- Only processes
multipart/form-data— it ignores any request with a different Content-Type - Does NOT process any form that is not multipart — you still need
express.json()for JSON - Adds
req.file(single upload) orreq.files(multiple uploads) - Populates
req.bodywith text fields from the multipart form - Stream-based — does not buffer the entire file in memory before writing (for disk storage)
4. Installing Multer
npm install multer
Basic setup in your Express app:
const express = require('express');
const multer = require('multer');
const app = express();
// Simplest configuration — files go to 'uploads/' folder
const upload = multer({ dest: 'uploads/' });
// Use express.json() for JSON routes — Multer for file routes
app.use(express.json());
// Apply upload middleware only on routes that need it
app.post('/api/upload', upload.single('avatar'), (req, res) => {
console.log(req.file); // file info object
console.log(req.body); // text fields
res.json({ message: 'File uploaded', file: req.file });
});
app.listen(3000, () => console.log('Server running on port 3000'));
Important: Do NOT use
app.use(upload.single('avatar'))globally. Multer should only be applied to specific routes that expect file uploads.
5. How Multer Works Internally
Client sends POST with multipart/form-data
│
▼
Express receives request
│
▼
Multer middleware intercepts (if Content-Type is multipart)
│
├──► Parses boundary string from Content-Type header
│
├──► Streams each part through busboy parser
│ │
│ ├── Text field? → Add to req.body
│ │
│ └── File field? → Run fileFilter
│ │
│ ├── Rejected? → Skip file, optionally throw error
│ │
│ └── Accepted? → Check size limits
│ │
│ ├── Disk storage → Stream to disk
│ │
│ └── Memory storage → Buffer in RAM
│
├──► Populate req.file / req.files
│
└──► Call next() → your route handler runs
The dest shortcut vs full storage configuration
// SHORTCUT — Multer generates random filenames, no extension
const upload = multer({ dest: 'uploads/' });
// File saved as: uploads/a1b2c3d4e5f6 (no extension!)
// FULL CONTROL — Use diskStorage for custom filenames
const storage = multer.diskStorage({
destination: (req, file, cb) => cb(null, 'uploads/'),
filename: (req, file, cb) => cb(null, Date.now() + '-' + file.originalname)
});
const upload = multer({ storage });
// File saved as: uploads/1699123456789-profile.jpg
6. Multer Configuration Object
The multer() function accepts a configuration object with these options:
const upload = multer({
// Option 1: Simple destination (auto-generated filenames)
dest: 'uploads/',
// Option 2: Full storage engine (overrides dest)
storage: multer.diskStorage({ /* ... */ }),
// File filter function — accept or reject files
fileFilter: (req, file, cb) => {
if (file.mimetype.startsWith('image/')) {
cb(null, true); // accept
} else {
cb(new Error('Only images are allowed'), false); // reject
}
},
// Size and count limits
limits: {
fileSize: 5 * 1024 * 1024, // 5 MB per file
files: 10, // max 10 files per request
fields: 20, // max 20 non-file fields
fieldSize: 1024 * 1024, // max 1 MB per text field value
headerPairs: 2000 // max header key-value pairs
},
// Preserve the path of the file (default: false)
preservePath: false
});
Configuration options reference
| Option | Type | Description |
|---|---|---|
dest | string | Destination folder (Multer auto-generates filenames) |
storage | StorageEngine | Full storage engine — overrides dest |
fileFilter | function | Function to control which files are accepted |
limits | object | Limits on file size, count, field size, etc. |
preservePath | boolean | Keep full path of files instead of just filename |
limits sub-options
| Property | Default | Description |
|---|---|---|
fieldNameSize | 100 bytes | Max field name size |
fieldSize | 1 MB | Max value of a text field |
fields | Infinity | Max number of non-file fields |
fileSize | Infinity | Max file size (bytes) |
files | Infinity | Max number of file fields |
parts | Infinity | Max total parts (fields + files) |
headerPairs | 2000 | Max header key-value pairs |
7. Upload Methods — single, array, fields, none
Multer provides four methods to handle different upload scenarios:
7.1 upload.single(fieldName) — One file
// HTML: <input type="file" name="avatar" />
app.post('/profile', upload.single('avatar'), (req, res) => {
console.log(req.file); // single file object
console.log(req.body); // text fields
res.json({ file: req.file });
});
req.file— object with file inforeq.body— object with text fields
7.2 upload.array(fieldName, maxCount) — Multiple files, same field
// HTML: <input type="file" name="photos" multiple />
app.post('/gallery', upload.array('photos', 10), (req, res) => {
console.log(req.files); // array of file objects (up to 10)
console.log(req.body); // text fields
res.json({ count: req.files.length });
});
req.files— array of file objectsmaxCount— maximum number of files (optional but recommended)
7.3 upload.fields(fields) — Multiple files, different fields
// HTML: separate file inputs for avatar and cover photo
const cpUpload = upload.fields([
{ name: 'avatar', maxCount: 1 },
{ name: 'gallery', maxCount: 8 }
]);
app.post('/profile', cpUpload, (req, res) => {
console.log(req.files['avatar']); // array with 1 file object
console.log(req.files['gallery']); // array with up to 8 file objects
console.log(req.body); // text fields
res.json({ message: 'Profile updated' });
});
req.files— object keyed by field name, each value is an array- Each field entry has
name(field name) and optionalmaxCount
7.4 upload.none() — Text-only multipart form
// Multipart form with no file inputs
app.post('/text-form', upload.none(), (req, res) => {
console.log(req.body); // text fields from multipart form
res.json(req.body);
});
req.file/req.files— undefined (no files expected)- Use case: When a form uses
enctype="multipart/form-data"but has no file input (rare but possible) - If a file IS sent with
upload.none(), Multer throws aLIMIT_UNEXPECTED_FILEerror
Method comparison
| Method | req.file | req.files | Use Case |
|---|---|---|---|
upload.single('name') | File object | — | Profile picture, document |
upload.array('name', max) | — | Array of file objects | Photo gallery, bulk import |
upload.fields([...]) | — | Object of arrays | Avatar + cover + gallery |
upload.none() | — | — | Multipart form without files |
upload.any() | — | Array of file objects | Accept any field name (avoid in production) |
Security note: Avoid
upload.any()in production — it accepts files from any field name, making it harder to validate and control uploads.
8. File Size Limits and File Type Filtering
Setting file size limits
const upload = multer({
dest: 'uploads/',
limits: {
fileSize: 5 * 1024 * 1024 // 5 MB
}
});
When a file exceeds the limit, Multer emits a LIMIT_FILE_SIZE error. You must catch it:
app.post('/upload', upload.single('file'), (req, res) => {
res.json({ file: req.file });
});
// Error handler for Multer errors
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. Maximum size is 5 MB.' });
}
return res.status(400).json({ error: err.message });
}
next(err);
});
File type filtering
const imageFilter = (req, file, cb) => {
// Accept only image files
if (file.mimetype.startsWith('image/')) {
cb(null, true); // accept the file
} else {
cb(new Error('Only image files are allowed!'), false); // reject
}
};
const upload = multer({
dest: 'uploads/',
fileFilter: imageFilter,
limits: { fileSize: 5 * 1024 * 1024 }
});
Common MIME type patterns
| Category | MIME Pattern | Examples |
|---|---|---|
| Images | image/* | image/jpeg, image/png, image/gif, image/webp |
| PDFs | application/pdf | application/pdf |
| Documents | application/msword, application/vnd.openxmlformats* | .doc, .docx |
| Videos | video/* | video/mp4, video/webm |
| Audio | audio/* | audio/mpeg, audio/wav |
Warning: MIME types from the client can be spoofed. For true security, verify the file content (magic bytes) on the server. See 3.7.d for details.
9. Key Takeaways
express.json()andexpress.urlencoded()cannot handle file uploads — you need Multer formultipart/form-data.- HTML forms need
enctype="multipart/form-data"when they include file inputs. - Multer is route-level middleware — apply it only to routes that accept files, never globally.
- Four upload methods:
single()for one file,array()for many same-field files,fields()for multiple named fields,none()for text-only multipart. - Always set
limits— uncapped file sizes are a denial-of-service risk. - Always set
fileFilter— accepting any file type is a security risk. destgives you auto-generated filenames (no extension); usediskStoragefor full control.
Explain-It Challenge
Can you explain to a friend: "Why can't Express handle file uploads by default, and what does Multer do about it?" If you can walk through the multipart encoding and Multer's parsing pipeline in under 90 seconds without notes, you have mastered this topic.