Handling multipart form data in Express is a surprisingly nuanced dance between the client, the server, and a few crucial middleware components.

Let’s see it in action. Imagine a simple HTML form that allows users to upload a profile picture.

<form action="/upload" method="post" enctype="multipart/form-data">
  <input type="text" name="username">
  <input type="file" name="profilePic">
  <button type="submit">Upload</button>
</form>

When this form is submitted, the browser doesn’t just send a simple key-value pair. Instead, it constructs a request body with distinct "parts," each containing its own headers and data. The enctype="multipart/form-data" tells the browser to use this format.

On the Express server, you need middleware to parse this incoming stream of data. The most common choice is multer.

First, install it:

npm install multer

Then, configure it in your Express app:

const express = require('express');
const multer = require('multer');
const path = require('path');

const app = express();
const upload = multer({ dest: 'uploads/' }); // Specify a destination directory

app.post('/upload', upload.fields([{ name: 'profilePic', maxCount: 1 }, { name: 'username', maxCount: 1 }]), (req, res) => {
  console.log('Username:', req.body.username);
  console.log('File details:', req.files.profilePic[0]);
  res.send('Upload successful!');
});

app.listen(3000, () => {
  console.log('Server listening on port 3000');
});

Here, multer intercepts the incoming request before your route handler. upload.fields(...) is a crucial part. It tells multer which form fields to expect and how many files are allowed for each. In this case, we’re expecting a single file named profilePic and a single text field named username.

When the request hits the /upload route, req.body will now contain the username field, and req.files will hold an object where req.files.profilePic is an array of file objects. Each file object contains information like fieldname, originalname, encoding, mimetype, destination, filename, and size.

The dest: 'uploads/' option tells multer where to temporarily store the uploaded files. It creates a directory named uploads in your project’s root if it doesn’t exist. The files are given a unique filename by multer to avoid collisions.

multer essentially reads the incoming request stream, identifies the boundaries between the different parts of the multipart data, parses the headers for each part (like Content-Disposition and Content-Type), and then either buffers the file content into memory or streams it directly to a specified destination on disk. It then populates req.body with the non-file form fields and req.files (or req.file for single uploads) with the file metadata and, if configured, the file content itself.

The upload.fields() method is flexible. You could use upload.single('profilePic') if you only expect one file field named profilePic, or upload.array('photos', 12) to accept multiple files under a single field name (photos) with a maximum of 12 files.

A common point of confusion is what happens to the actual file data. By default, multer saves the files to the dest directory. If you want to process the file in memory (e.g., to resize an image before saving), you can configure multer to use storage: multer.memoryStorage(). This will put the file content into req.files.profilePic[0].buffer instead of saving it to disk.

When dealing with large files, be mindful of memory usage. multer offers options like limits to restrict file size ({ fileSize: 1024 * 1024 * 5 } for 5MB) or the number of files. If you don’t specify a storage option, multer defaults to diskStorage, which is generally preferred for larger files to avoid overwhelming your server’s RAM.

The next hurdle you’ll often encounter is handling file uploads from different client-side frameworks or API clients that might not set the Content-Type header correctly or might structure their multipart requests in a slightly non-standard way, requiring you to dig into multer’s configuration or even implement custom parsing logic.

Want structured learning?

Take the full Express course →