Handling file uploads

Express · Advanced · 13 min

Uploads are a common feature and a common attack vector; limits and validation are not optional.

HTML forms and FormData send files as multipart/form-data, which express.json() and express.urlencoded() don't parse. multer, the Express team's middleware, does.

const upload = multer({ dest: "uploads/", limits: { fileSize: 5 * 1024 * 1024 } });
app.post("/avatar", upload.single("avatar"), handler);
  • upload.single(field) puts one file on req.file; upload.array(field, max) and upload.fields([...]) put several on req.files. Text fields land on req.body.
  • Each file object has originalname, mimetype, size, and either path (disk storage) or buffer (memory storage).

Storage choices:

  • Disk (dest or multer.diskStorage) — files are streamed to disk, so memory stays flat. Multer generates random filenames; never use originalname as a path.
  • Memory (multer.memoryStorage()) — convenient for small files you process immediately (resize an avatar), dangerous for large ones because the whole file sits in RAM.
  • For S3-like storage, stream from disk after validation, or use a storage engine that streams directly. Many apps skip the server entirely: hand the client a presigned upload URL and only store the resulting key.

Always set limits — fileSize, files, fields — or a single request can fill your disk or memory. Exceeding them raises a MulterError with a code like LIMIT_FILE_SIZE, which you map to 413 in your error handler.

mimetype and the extension come from the client and can lie. Use fileFilter for a first check, then verify content (magic bytes, or decode the image) before trusting it. Serve uploaded files from a separate domain or with Content-Disposition: attachment so a malicious HTML upload can't run in your origin.

Example

import express from "express";
import multer from "multer";
import path from "node:path";

const app = express();
const upload = multer({
  dest: path.join(import.meta.dirname, "uploads"),       // random filenames, streamed to disk
  limits: { fileSize: 2 * 1024 * 1024, files: 1 },        // 2 MB, one file
  fileFilter(req, file, cb) {
    const ok = ["image/png", "image/jpeg", "image/webp"].includes(file.mimetype);
    cb(ok ? null : Object.assign(new Error("only PNG, JPEG or WebP"), { status: 415 }), ok);
  },
});

app.post("/avatar", upload.single("avatar"), (req, res) => {
  if (!req.file) return res.status(400).json({ error: "avatar file is required" });
  // TODO: verify magic bytes / decode image before trusting it
  res.status(201).json({ stored: req.file.filename, size: req.file.size, caption: req.body.caption });
});

app.use((err, req, res, next) => {
  if (err instanceof multer.MulterError) {
    return res.status(err.code === "LIMIT_FILE_SIZE" ? 413 : 400).json({ error: err.code });
  }
  res.status(err.status ?? 500).json({ error: err.status ? err.message : "Internal Server Error" });
});

app.listen(3000);
// curl -F avatar=@me.png -F caption=hi localhost:3000/avatar

Remember

  • multer parses multipart/form-data; req.file/req.files plus req.body
  • Always set limits; map MulterError codes to 400/413
  • Disk storage for size, memory only for small files; never trust originalname
  • Client MIME types lie — verify content before use

Watch out for

  • Using memoryStorage without a fileSize limit, letting one upload exhaust RAM.
  • Saving files under the client-supplied original name, enabling overwrites and path tricks.

Check yourself

  1. Why is multer's memoryStorage risky for a video-upload endpoint?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: expressjs.com/en/resources/middleware/multer