multer File Uploads
HTML forms that upload files send their data as multipart/form-data, an encoding that the built-in express.json() and express.urlencoded() parsers cannot read. multer is the official Express middleware that parses these requests, exposing text fields on req.body and uploaded files on req.file or req.files. It is built on Busboy, supports disk and in-memory storage, and lets you enforce size limits and reject unwanted file types before they ever touch your handler.
Installing and choosing a storage engine
Install multer, then create an instance configured with a storage engine. The engine decides where each incoming file goes. multer ships two built-ins: diskStorage writes files to the filesystem and gives you req.file.path, while memoryStorage keeps the file in a Buffer on req.file.buffer — ideal when you immediately stream the bytes to S3 or another service rather than holding them on disk.
npm install multer
const express = require("express");
const multer = require("multer");
const path = require("path");
const app = express();
const storage = multer.diskStorage({
destination: (req, file, cb) => cb(null, "uploads/"),
filename: (req, file, cb) => {
const unique = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
cb(null, `${unique}${path.extname(file.originalname)}`);
},
});
const upload = multer({ storage });
Always generate your own
filename. The default disk engine assigns a random name with no extension, and trusting the client’soriginalnamedirectly lets an attacker write paths like../../etc/passwd. Derive the name yourself and sanitize the extension.
If you call multer({ dest: "uploads/" }) instead of supplying storage, multer uses diskStorage with default (extension-less) filenames. For full control, configure diskStorage as shown above.
Single file uploads
upload.single(fieldname) accepts one file from the named form field and attaches it to req.file. Any non-file text fields in the same form land on req.body. The middleware runs before your handler, so by the time the route body executes the file is already saved.
app.post("/avatar", upload.single("avatar"), async (req, res) => {
if (!req.file) {
return res.status(400).json({ error: "No file uploaded" });
}
await saveProfile(req.body.userId, req.file.path);
res.status(201).json({
field: req.file.fieldname,
name: req.file.originalname,
size: req.file.size,
storedAs: req.file.filename,
});
});
Output:
POST /avatar (multipart/form-data: avatar=<photo.png>, userId=42)
HTTP/1.1 201 Created
{
"field": "avatar",
"name": "photo.png",
"size": 84213,
"storedAs": "1718323200000-512837465.png"
}
The req.file object exposes a consistent shape regardless of engine:
| Property | Description |
|---|---|
fieldname | Form field name the file came from |
originalname | Filename on the client’s machine |
mimetype | Reported MIME type, e.g. image/png |
size | File size in bytes |
path | Full path on disk (diskStorage only) |
filename | Name multer stored it under (diskStorage only) |
buffer | File contents as a Buffer (memoryStorage only) |
Multiple files and mixed fields
For several files from one field, use upload.array(fieldname, maxCount); multer collects them into req.files as an array. When a form has different file fields — say one image and several attachments — use upload.fields() with an array of field specs, and req.files becomes an object keyed by field name.
// Many files under a single "photos" field
app.post("/gallery", upload.array("photos", 8), (req, res) => {
const names = req.files.map((f) => f.originalname);
res.json({ count: req.files.length, names });
});
// Distinct fields, each with its own limit
const uploadMixed = upload.fields([
{ name: "cover", maxCount: 1 },
{ name: "attachments", maxCount: 5 },
]);
app.post("/post", uploadMixed, (req, res) => {
res.json({
cover: req.files.cover?.[0]?.originalname,
attachments: (req.files.attachments || []).map((f) => f.originalname),
});
});
If you expect text fields but no files at all, upload.none() parses the multipart/form-data body into req.body without accepting any uploads. Use upload.any() only with care — it accepts files from every field and is easy to abuse.
Limits and file filters
Two controls keep uploads safe: limits caps sizes and counts at the parser level, and fileFilter accepts or rejects each file by inspecting its metadata. The filter runs before the file is written, so rejecting a bad upload costs nothing.
const upload = multer({
storage,
limits: {
fileSize: 5 * 1024 * 1024, // 5 MB per file
files: 10, // max number of files
},
fileFilter: (req, file, cb) => {
const allowed = ["image/jpeg", "image/png", "image/webp"];
if (allowed.includes(file.mimetype)) {
cb(null, true); // accept
} else {
cb(new Error("Only JPEG, PNG, and WebP images are allowed"));
}
},
});
When a limit is exceeded, multer throws a MulterError with a machine-readable code. Catch it in an error-handling middleware so clients get a clean response instead of a stack trace.
app.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
return res.status(400).json({ error: err.code }); // e.g. LIMIT_FILE_SIZE
}
if (err) return res.status(400).json({ error: err.message });
next();
});
Output:
POST /gallery (file is 12 MB, limit is 5 MB)
HTTP/1.1 400 Bad Request
{ "error": "LIMIT_FILE_SIZE" }
multer parses fields in the order they appear in the request. Because
fileFilteronly sees fields that arrive before a file, put your text fields ahead of file fields in the form if your filter logic depends onreq.body.
Memory storage for cloud uploads
When you forward files to object storage rather than keeping them locally, memoryStorage avoids the round trip through disk. Each file’s bytes are available on req.file.buffer, ready to pipe straight to your provider.
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 10 * 1024 * 1024 },
});
app.post("/documents", upload.single("doc"), async (req, res) => {
const url = await uploadToS3({
body: req.file.buffer,
contentType: req.file.mimetype,
key: `docs/${Date.now()}-${req.file.originalname}`,
});
res.status(201).json({ url });
});
Keep memory storage behind a strict fileSize limit — without it, a large upload is buffered entirely in RAM and can exhaust the process.
Best Practices
- Always set a
fileSizelimit; an unbounded upload endpoint is a denial-of-service vector, especially withmemoryStorage. - Generate your own
filenameand never interpolatereq.file.originalnameinto a path — sanitize the extension and store outside the web root. - Validate type with
fileFilter, but treatmimetypeas a hint; verify magic bytes server-side for anything security-sensitive. - Mount multer per-route, not globally with
app.use— only the routes that accept uploads should run the parser. - Add a
MulterError-aware error handler so size and count violations return clean400responses instead of crashing the request. - Prefer
memoryStoragewhen relaying to S3 or similar; reach fordiskStorageonly when you truly need a file on the local filesystem. - Place text fields before file fields in your forms when
fileFilterreadsreq.body, since multer streams fields in arrival order.