
File uploads arrive at your server in a format called multipart/form-data that Express can't parse on its own. Multer is the middleware that bridges that gap. This post covers how multer works, how to handle single and multiple files, and how to configure storage.
When a user submits a form with a text field, the browser packages the data as a simple key-value string. When that same form has a file input, the browser switches to a completely different encoding format: multipart/form-data. It splits the request into multiple parts, each with its own headers and content. Express's built-in body parsers don't know what to do with that format. That's exactly the problem multer solves.
A standard JSON body is clean and flat:
Content-Type: application/json { "name": "Atharv", "role": "admin" }
A multipart request looks like this under the hood:
Content-Type: multipart/form-data; boundary=----FormBoundary7MA4 ------FormBoundary7MA4 Content-Disposition: form-data; name="username" Atharv ------FormBoundary7MA4 Content-Disposition: form-data; name="avatar"; filename="photo.jpg" Content-Type: image/jpeg <binary image data here> ------FormBoundary7MA4--
Each part has its own headers and content. Text fields and files live side by side. Parsing this correctly, streaming the binary parts to disk or memory, requires dedicated middleware. That's multer.
bashnpm install multer
The simplest possible setup uses memory storage, where files are held in a Buffer in RAM:
javascriptconst express = require("express"); const multer = require("multer"); const app = express(); const upload = multer({ dest: "uploads/" }); // files go to uploads/ folder
dest is the quick-start option. It saves files to disk with auto-generated filenames (no extension). For anything beyond prototyping, use diskStorage for control.
Multer integrates into Express as route-level middleware. It runs between the incoming request and your route handler:
┌────────────────────────────────────────────────────────────┐ │ Request Lifecycle │ │ │ │ Client sends multipart/form-data │ │ │ │ │ ▼ │ │ ┌───────────────┐ │ │ │ multer │ ← parses multipart body │ │ │ middleware │ streams file to disk or memory │ │ │ │ populates req.file / req.files │ │ └──────┬────────┘ populates req.body (text fields) │ │ │ │ │ ▼ │ │ ┌───────────────┐ │ │ │ Route Handler │ ← req.file and req.body ready to use │ │ └───────────────┘ │ └────────────────────────────────────────────────────────────┘
Text fields from the multipart form go to req.body. Files go to req.file (single) or req.files (multiple). Without multer, both would be missing entirely.
upload.single(fieldName) processes one file from the named input field:
javascriptLoading syntax highlighter...
req.file is undefined if the client didn't include a file, so always check for it.
Three multer methods handle different multi-file scenarios:
javascriptLoading syntax highlighter...
In practice, upload.array() handles gallery or batch uploads, and upload.fields() handles forms with distinct file inputs (avatar + ID document, for example):
javascriptLoading syntax highlighter...
Multer offers two storage engines:
┌─────────────────────────┬─────────────────────────────┐ │ multer.memoryStorage │ multer.diskStorage │ ├─────────────────────────┼─────────────────────────────┤ │ File stored in Buffer │ File written to disk │ │ req.file.buffer exists │ req.file.path exists │ │ No disk write │ Persists between requests │ │ Good for cloud uploads │ Good for local serving │ │ Risk: large files OOM │ Risk: disk space │ └─────────────────────────┴─────────────────────────────┘
Memory storage is useful when you plan to immediately forward the file elsewhere, like streaming it to S3. The buffer is right there in memory, no extra file read needed. Disk storage is the default choice when you're serving files from your own server.
Multer lets you filter incoming files before they're saved:
javascriptLoading syntax highlighter...
limits.fileSize cuts off the upload stream before an oversized file reaches disk. fileFilter runs before storage and can accept or reject based on MIME type, field name, or any logic you need.
Once saved to disk, make the file accessible with express.static:
javascriptapp.use("/uploads", express.static(path.join(__dirname, "uploads")));
A file saved to uploads/1721234567890-avatar.jpg becomes available at:
http://localhost:3000/uploads/1721234567890-avatar.jpg
Return this URL in your upload response and the client can use it immediately.
multipart/form-data, a format Express can't parse without middleware.req.file and req.files.upload.single() for one file, upload.array() for multiple files from one field, upload.fields() for files across multiple named inputs.diskStorage gives you control over filename and destination. Always generate your own filename: never use file.originalname directly.fileFilter (MIME type) and limits.fileSize before files reach storage.Multer's API is small but covers every realistic upload scenario. Once you understand the middleware flow and which method maps to which use case, the rest is just configuration.
Related posts based on tags, category, and projects
Handling file uploads in Express involves two things: storing the file somewhere on disk and making it accessible via a URL. This post covers both, using multer for uploads and Express's static file serving for access.
Middleware is code that runs between a request arriving and a response being sent. In Express, every middleware function in the chain gets a chance to inspect, modify, or stop a request. This post covers what middleware is, how `next()` controls the flow, and where it gets used in real applications.
REST is a set of conventions for designing APIs that are predictable, consistent, and easy to work with. This post covers what REST means, how it maps to HTTP, and how to build a clean resource-based API in Express.
JWT lets your server verify who a user is without storing anything in a database. This post walks through what a JWT is, how the login flow works, and how to protect routes using tokens in Node.js.
const storage = multer.diskStorage({
destination: (req, file, cb) => cb(null, "uploads/"),
filename: (req, file, cb) => {
const ext = path.extname(file.originalname);
cb(null, `${Date.now()}-${file.fieldname}${ext}`);
},
});
const upload = multer({ storage });
app.post("/profile/avatar", upload.single("avatar"), (req, res) => {
console.log(req.file);
// {
// fieldname: 'avatar',
// originalname: 'photo.jpg',
// mimetype: 'image/jpeg',
// filename: '1721234567890-avatar.jpg',
// path: 'uploads/1721234567890-avatar.jpg',
// size: 204800
// }
if (!req.file) {
return res.status(400).json({ error: "No file provided" });
}
res.status(201).json({
message: "Avatar uploaded",
path: req.file.path,
});
});// Multiple files from one field (max 5)
upload.array("photos", 5);
// req.files → array of file objects
// Multiple files from different fields
upload.fields([
{ name: "avatar", maxCount: 1 },
{ name: "gallery", maxCount: 4 },
]);
// req.files → object keyed by field name
// req.files['avatar'][0], req.files['gallery'][0..3]
// Accept any files regardless of field name
upload.any();
// req.files → array, each has a fieldname propertyapp.post(
"/onboarding",
upload.fields([
{ name: "avatar", maxCount: 1 },
{ name: "idDocument", maxCount: 1 },
]),
(req, res) => {
const avatar = req.files["avatar"]?.[0];
const idDoc = req.files["idDocument"]?.[0];
if (!avatar || !idDoc) {
return res.status(400).json({ error: "Both files required" });
}
res.json({
avatarPath: avatar.path,
idPath: idDoc.path,
});
},
);const upload = multer({
storage,
limits: { fileSize: 2 * 1024 * 1024 }, // 2MB
fileFilter: (req, file, cb) => {
const allowed = ["image/jpeg", "image/png", "image/webp"];
if (allowed.includes(file.mimetype)) {
cb(null, true); // accept
} else {
cb(null, false); // silently reject
// or cb(new Error('Invalid file type')) to throw
}
},
});