Skip to content
Express.js ex libraries 5 min read

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’s originalname directly 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:

PropertyDescription
fieldnameForm field name the file came from
originalnameFilename on the client’s machine
mimetypeReported MIME type, e.g. image/png
sizeFile size in bytes
pathFull path on disk (diskStorage only)
filenameName multer stored it under (diskStorage only)
bufferFile 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 fileFilter only sees fields that arrive before a file, put your text fields ahead of file fields in the form if your filter logic depends on req.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 fileSize limit; an unbounded upload endpoint is a denial-of-service vector, especially with memoryStorage.
  • Generate your own filename and never interpolate req.file.originalname into a path — sanitize the extension and store outside the web root.
  • Validate type with fileFilter, but treat mimetype as 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 clean 400 responses instead of crashing the request.
  • Prefer memoryStorage when relaying to S3 or similar; reach for diskStorage only when you truly need a file on the local filesystem.
  • Place text fields before file fields in your forms when fileFilter reads req.body, since multer streams fields in arrival order.
Last updated June 14, 2026
Was this helpful?