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.
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].
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.
npm install multerMulter 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.
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.
| 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 |
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.
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:
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);
});res.sendFile() rather than express.static.file-type.uploads/ to .gitignore and create it at startup with fs.mkdirSync("uploads", { recursive: true }).Which form attribute is required for a browser to send file contents?
express.urlencoded() parses classic form posts; in Express 5 pass extended: true for nested fields.multipart/form-data; choose diskStorage for local files or memoryStorage for cloud forwarding.single, array, fields or none to match the shape of the form, and read results from req.file / req.files.limits and a fileFilter, generate your own filenames, and translate MulterError into a 400 response.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.