Form Handling and File Uploads with Multer

Intermediate
13 min

Form Handling and File Uploads with Multer

HTML forms send data in two encodings: application/x-www-form-urlencoded for plain fields and multipart/form-data when a file is attached. Express parses the first out of the box; the second needs Multer. After this lesson you will be able to accept classic form posts, save uploads safely, enforce size and type limits, and return clear errors when an upload is rejected.

Parsing regular form posts

express.urlencoded() reads URL-encoded bodies into req.body. In Express 5 the extended option defaults to false, which uses Node's querystring parser. Set it to true to allow nested fields such as address[city].

javascript
import express from "express"; const app = express(); app.use(express.urlencoded({ extended: true })); app.get("/contact", (req, res) => { res.send(` <form method="POST" action="/contact"> <input name="email" type="email" required /> <textarea name="message"></textarea> <button>Send</button> </form>`); }); app.post("/contact", (req, res) => { const { email, message } = req.body; // save, send mail, then Post/Redirect/Get to avoid duplicate submits res.redirect(303, "/contact?sent=1"); });

Responding with a 303 redirect after a successful POST is the Post/Redirect/Get pattern: a browser refresh reloads the GET page instead of re-submitting the form.

Installing and configuring Multer

bash
npm install multer

Multer only processes multipart/form-data. It adds the text fields to req.body and the file(s) to req.file or req.files. The most important configuration decisions are where files go and what is allowed.

javascript
import multer from "multer"; import path from "node:path"; const storage = multer.diskStorage({ destination: "uploads/", filename: (req, file, cb) => { const ext = path.extname(file.originalname).toLowerCase(); cb(null, `${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}`); }, }); const upload = multer({ storage, limits: { fileSize: 2 * 1024 * 1024, files: 5 }, fileFilter: (req, file, cb) => { const ok = ["image/png", "image/jpeg", "image/webp"].includes(file.mimetype); cb(ok ? null : new Error("Only PNG, JPEG or WebP images are allowed"), ok); }, });

Never reuse file.originalname as the stored filename: it is user-controlled and may contain path separators or collide with another user's upload. Generate your own name and keep only the extension.

Accepting one, many or mixed files

| Method | Form field(s) | Result | | --- | --- | --- | | upload.single("avatar") | one file input named avatar | req.file | | upload.array("photos", 5) | one input with multiple, up to 5 files | req.files (array) | | upload.fields([{ name: "cover", maxCount: 1 }, { name: "gallery", maxCount: 8 }]) | several inputs | req.files.cover, req.files.gallery | | upload.none() | text fields only | req.body |

javascript
app.post("/products", upload.array("photos", 5), (req, res) => { res.status(201).json({ title: req.body.title, photos: req.files.map((f) => `/uploads/${f.filename}`), }); }); app.use("/uploads", express.static("uploads"));

The HTML form must declare enctype="multipart/form-data"; without it the browser sends only the file names as text fields.

When files are forwarded to S3, Cloudinary or another object store, use multer.memoryStorage() instead of disk: req.file.buffer holds the bytes you stream to the provider's SDK. Keep limits.fileSize small in that mode, because every in-flight upload lives in RAM.

Handling upload errors

Multer rejects oversized or unexpected files by passing a MulterError to next(). Catch it in your error middleware so the client gets a 400 instead of a generic 500:

javascript
app.use((err, req, res, next) => { if (err instanceof multer.MulterError) { const message = err.code === "LIMIT_FILE_SIZE" ? "File too large (max 2 MB)" : err.message; return res.status(400).json({ error: message }); } if (err.message?.startsWith("Only PNG")) { return res.status(415).json({ error: err.message }); } next(err); });

Tips

  • Serve private files through an authenticated route with res.sendFile() rather than express.static.
  • MIME types come from the client; for strict checks inspect the file's magic bytes with a library such as file-type.
  • Add uploads/ to .gitignore and create it at startup with fs.mkdirSync("uploads", { recursive: true }).
Quick Quiz
Question 1 of 3

Which form attribute is required for a browser to send file contents?

Key Takeaways

  • express.urlencoded() parses classic form posts; in Express 5 pass extended: true for nested fields.
  • Multer parses multipart/form-data; choose diskStorage for local files or memoryStorage for cloud forwarding.
  • Use single, array, fields or none to match the shape of the form, and read results from req.file / req.files.
  • Enforce limits and a fileFilter, generate your own filenames, and translate MulterError into a 400 response.
  • Redirect with 303 after a successful POST to avoid duplicate submissions.

Next lesson: Express Router and Project Structure — split routes into modules with express.Router() and organise a growing codebase.

Form Handling and File Uploads with Multer - Express.js | CodeYourCraft | CodeYourCraft