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

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:

ParserContent-Type It HandlesCan Handle Files?
express.json()application/jsonNo
express.urlencoded()application/x-www-form-urlencodedNo
Multermultipart/form-dataYes
┌─────────────────────────────────────────────────────────────────┐
│  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.body or req.file on a multipart request without Multer, you get undefined. 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

Encodingenctype ValueUse Case
URL-encodedapplication/x-www-form-urlencodedText-only forms (login, search)
Multipartmultipart/form-dataForms with file uploads
JSONapplication/jsonJavaScript API calls (fetch/axios)

Rule of thumb: If your form has <input type="file">, you MUST set enctype="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) or req.files (multiple uploads)
  • Populates req.body with 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

OptionTypeDescription
deststringDestination folder (Multer auto-generates filenames)
storageStorageEngineFull storage engine — overrides dest
fileFilterfunctionFunction to control which files are accepted
limitsobjectLimits on file size, count, field size, etc.
preservePathbooleanKeep full path of files instead of just filename

limits sub-options

PropertyDefaultDescription
fieldNameSize100 bytesMax field name size
fieldSize1 MBMax value of a text field
fieldsInfinityMax number of non-file fields
fileSizeInfinityMax file size (bytes)
filesInfinityMax number of file fields
partsInfinityMax total parts (fields + files)
headerPairs2000Max 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 info
  • req.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 objects
  • maxCount — 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 optional maxCount

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 a LIMIT_UNEXPECTED_FILE error

Method comparison

Methodreq.filereq.filesUse Case
upload.single('name')File objectProfile picture, document
upload.array('name', max)Array of file objectsPhoto gallery, bulk import
upload.fields([...])Object of arraysAvatar + cover + gallery
upload.none()Multipart form without files
upload.any()Array of file objectsAccept 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

CategoryMIME PatternExamples
Imagesimage/*image/jpeg, image/png, image/gif, image/webp
PDFsapplication/pdfapplication/pdf
Documentsapplication/msword, application/vnd.openxmlformats*.doc, .docx
Videosvideo/*video/mp4, video/webm
Audioaudio/*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

  1. express.json() and express.urlencoded() cannot handle file uploads — you need Multer for multipart/form-data.
  2. HTML forms need enctype="multipart/form-data" when they include file inputs.
  3. Multer is route-level middleware — apply it only to routes that accept files, never globally.
  4. Four upload methods: single() for one file, array() for many same-field files, fields() for multiple named fields, none() for text-only multipart.
  5. Always set limits — uncapped file sizes are a denial-of-service risk.
  6. Always set fileFilter — accepting any file type is a security risk.
  7. dest gives you auto-generated filenames (no extension); use diskStorage for 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.


Next → 3.7.b — File Upload with Multer