AI News Hub Logo

AI News Hub

Handling File Uploads in Express with Multer

DEV Community
Pratham

How to accept profile pictures, documents, and any file your users throw at your server. The first time I tried to handle a file upload in Express, I wrote something like this: app.post("/upload", (req, res) => { console.log(req.body); // Where's my file? }); I submitted a form with an image attached, checked req.body, and got... nothing. An empty object. The file had vanished into the void. Turns out, express.json() and express.urlencoded() can't handle files. Files are sent in a completely different format called multipart/form-data, and Express has no built-in way to parse it. You need a middleware specifically designed for file uploads. That middleware is Multer — and once I set it up in the ChaiCode Web Dev Cohort 2026, file uploads went from mysterious to straightforward. Let me show you. When you send JSON data to Express, the body looks like this: Content-Type: application/json {"name": "Pratham", "email": "[email protected]"} express.json() knows how to parse this. Simple text, simple format. But when you send a file, the body looks completely different: Content-Type: multipart/form-data; boundary=----abc123 ------abc123 Content-Disposition: form-data; name="name" Pratham ------abc123 Content-Disposition: form-data; name="avatar"; filename="photo.jpg" Content-Type: image/jpeg (binary image data — thousands of bytes of raw binary) ------abc123-- This is multipart/form-data — a format that can carry both text fields and binary file data in the same request. It splits the body into parts (separated by a boundary string), each with its own headers and content. express.json() can't parse this. It sees binary data and has no idea what to do with it. You need a dedicated multipart parser — and that's Multer. Form Data Types: application/json → Text only. Parsed by express.json() application/x-www-form-urlencoded → Text only (form fields). Parsed by express.urlencoded() multipart/form-data → Text AND binary files. Parsed by MULTER Multer is a Node.js middleware for handling multipart/form-data. It processes file uploads and makes them available on req.file (single file) or req.files (multiple files). npm install multer Without Multer: req.body → {} (empty — file data is lost) req.file → undefined With Multer: req.body → { name: "Pratham" } (text fields parsed) req.file → { fieldname: "avatar", originalname: "photo.jpg", mimetype: "image/jpeg", size: 245678, path: "uploads/abc123-photo.jpg" } Multer parses the multipart request, extracts the file, saves it to disk (or keeps it in memory), and gives you all the file information on req.file. Let's start with the most common scenario: uploading one file — like a profile picture. const express = require("express"); const multer = require("multer"); const app = express(); app.use(express.json()); // Configure multer — save files to "uploads/" folder const upload = multer({ dest: "uploads/" }); // Single file upload route app.post("/upload/avatar", upload.single("avatar"), (req, res) => { console.log("File:", req.file); console.log("Body:", req.body); res.json({ message: "File uploaded successfully!", file: { originalName: req.file.originalname, size: req.file.size, path: req.file.path, }, }); }); app.listen(3000, () => console.log("Server on http://localhost:3000")); multer({ dest: "uploads/" }) — tells Multer where to save files upload.single("avatar") — expects ONE file in a field called "avatar" After processing, file info is on req.file The req.file Object { fieldname: "avatar", // Name of the form field originalname: "photo.jpg", // Original filename from the user encoding: "7bit", // File encoding mimetype: "image/jpeg", // File type destination: "uploads/", // Where it was saved filename: "a1b2c3d4e5f6", // Generated filename (no extension!) path: "uploads/a1b2c3d4e5f6", // Full path to saved file size: 245678 // File size in bytes } curl -X POST http://localhost:3000/upload/avatar \ -F "avatar=@/path/to/photo.jpg" \ -F "name=Pratham" -F sends form data in multipart format. @ reads from a file. Upload The enctype="multipart/form-data" attribute is essential — without it, the browser won't send the file as binary data. ┌────────────────┐ │ Client │ │ │ │ │ │ enctype= │ │ "multipart/ │ │ form-data" │ │ │ │ [photo.jpg] │ │ [name field] │ └───────┬────────┘ │ │ POST /upload/avatar │ Content-Type: multipart/form-data ↓ ┌────────────────────────────────────────────┐ │ EXPRESS SERVER │ │ │ │ 1. Request arrives │ │ │ │ 2. Multer middleware runs: │ │ → Parses multipart body │ │ → Extracts file binary data │ │ → Saves file to disk (uploads/) │ │ → Populates req.file with metadata │ │ → Populates req.body with text fields │ │ │ │ 3. Route handler runs: │ │ → Reads req.file and req.body │ │ → Sends response │ │ │ └───────────────────┬────────────────────────┘ │ ↓ ┌───────────────────┐ │ uploads/ folder │ │ │ │ a1b2c3d4e5f6 │ ← saved file │ (photo.jpg data) │ └───────────────────┘ For uploading multiple images at once (like a photo gallery): // Accept up to 5 files from the "photos" field app.post("/upload/gallery", upload.array("photos", 5), (req, res) => { console.log("Files:", req.files); // Array of file objects res.json({ message: `${req.files.length} files uploaded!`, files: req.files.map((f) => ({ name: f.originalname, size: f.size, })), }); }); upload.array("photos", 5) — accepts up to 5 files from the "photos" field req.files is an array of file objects (same shape as req.file) For forms with separate file inputs (avatar + resume): const multiUpload = upload.fields([ { name: "avatar", maxCount: 1 }, { name: "resume", maxCount: 1 }, ]); app.post("/upload/profile", multiUpload, (req, res) => { console.log("Avatar:", req.files["avatar"][0]); console.log("Resume:", req.files["resume"][0]); res.json({ avatar: req.files["avatar"][0].originalname, resume: req.files["resume"][0].originalname, }); }); Method Use Case Access Files Via upload.single("fieldName") One file req.file upload.array("fieldName", max) Multiple files, same field req.files (array) upload.fields([{ name, maxCount }]) Multiple files, different fields req.files["name"] upload.none() No files (text fields only) req.body By default, multer({ dest: "uploads/" }) saves files with random names and no extension. For more control, use Multer's disk storage engine. const storage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, "uploads/"); // Save to uploads/ folder }, filename: (req, file, cb) => { // Create unique filename: timestamp-originalname const uniqueName = `${Date.now()}-${file.originalname}`; cb(null, uniqueName); }, }); const upload = multer({ storage }); Now files are saved as 1715350000000-photo.jpg instead of a1b2c3d4e5f6. const upload = multer({ storage, limits: { fileSize: 5 * 1024 * 1024, // 5 MB max }, fileFilter: (req, file, cb) => { // Only allow images const allowedTypes = ["image/jpeg", "image/png", "image/gif", "image/webp"]; if (allowedTypes.includes(file.mimetype)) { cb(null, true); // Accept } else { cb(new Error("Only image files are allowed!"), false); // Reject } }, }); app.post("/upload/avatar", (req, res) => { upload.single("avatar")(req, res, (err) => { if (err instanceof multer.MulterError) { // Multer-specific error (file too large, too many files, etc.) return res.status(400).json({ error: err.message }); } if (err) { // Custom error (wrong file type) return res.status(400).json({ error: err.message }); } if (!req.file) { return res.status(400).json({ error: "No file uploaded" }); } res.json({ message: "Upload successful!", file: req.file.originalname }); }); }); After uploading, you need to make the files accessible. Use Express's static file middleware: // Serve the uploads folder as static files app.use("/uploads", express.static("uploads")); Now files are accessible via URL: File saved at: uploads/1715350000000-photo.jpg Accessible at: http://localhost:3000/uploads/1715350000000-photo.jpg const express = require("express"); const multer = require("multer"); const path = require("path"); const app = express(); app.use(express.json()); // Storage config const storage = multer.diskStorage({ destination: "uploads/", filename: (req, file, cb) => { const ext = path.extname(file.originalname); cb(null, `${Date.now()}-${Math.round(Math.random() * 1000)}${ext}`); }, }); const upload = multer({ storage, limits: { fileSize: 5 * 1024 * 1024 }, fileFilter: (req, file, cb) => { const allowed = [".jpg", ".jpeg", ".png", ".gif", ".webp"]; const ext = path.extname(file.originalname).toLowerCase(); if (allowed.includes(ext)) cb(null, true); else cb(new Error(`File type ${ext} not allowed`)); }, }); // Serve uploads as static files app.use("/uploads", express.static("uploads")); // Upload endpoint app.post("/api/upload", upload.single("image"), (req, res) => { if (!req.file) return res.status(400).json({ error: "No file" }); const fileUrl = `${req.protocol}://${req.get("host")}/uploads/${req.file.filename}`; res.status(201).json({ message: "Uploaded!", file: { name: req.file.originalname, size: `${(req.file.size / 1024).toFixed(1)} KB`, url: fileUrl, }, }); }); // Serve upload form app.get("/", (req, res) => { res.send(` File Upload Upload `); }); app.listen(3000, () => console.log("Server on http://localhost:3000")); Visit http://localhost:3000, upload an image, and the response will include a URL where you can view it. POST /api/upload (with file attached) │ ↓ ┌───────────────────────────────────────────────┐ │ MULTER MIDDLEWARE │ │ │ │ 1. Check Content-Type │ │ → Is it multipart/form-data? │ │ → NO → skip (or error) │ │ → YES → continue │ │ │ │ 2. Parse the multipart body │ │ → Separate text fields and file data │ │ │ │ 3. File filter check │ │ → Is this file type allowed? │ │ → NO → reject with error │ │ → YES → continue │ │ │ │ 4. Size limit check │ │ → Is file within size limit? │ │ → NO → reject with MulterError │ │ → YES → continue │ │ │ │ 5. Storage engine │ │ → Determine destination folder │ │ → Generate filename │ │ → Write file to disk │ │ │ │ 6. Populate request object │ │ → req.file = { originalname, path, ... } │ │ → req.body = { text fields } │ │ │ │ 7. Call next() │ │ → Route handler runs │ │ │ └───────────────────────────────────────────────┘ Build a server that: Accepts a profile picture via POST /upload/avatar Saves it to an uploads/ folder with a unique name Returns the file URL in the response Add a route POST /upload/gallery that: Accepts up to 5 images Returns info about all uploaded files Modify your upload to: Only accept images (JPEG, PNG, GIF, WebP) Limit file size to 2 MB Return clear error messages when validation fails Serve the uploads/ folder with express.static() Create a GET /gallery route that returns HTML showing all uploaded images Files need special handling because they're sent as multipart/form-data — a format that express.json() can't parse. Multer is the middleware that bridges this gap. upload.single("field") handles one file (req.file). upload.array("field", max) handles multiple files from the same field (req.files). upload.fields() handles files from different fields. Storage configuration controls where and how files are saved. Use multer.diskStorage() for custom filenames and destinations. Always validate uploads — check file type with fileFilter, limit file size with limits, and handle errors gracefully. Serve uploads with express.static("uploads") to make files accessible via URL. File uploads are one of those features that seem intimidating until you set up Multer — then it's just another middleware in your Express pipeline. Parse the multipart data, validate the file, save it, serve it. The same pattern whether you're handling profile pictures, document uploads, or image galleries. I'm learning all of this through the ChaiCode Web Dev Cohort 2026 under Hitesh Chaudhary and Piyush Garg. File uploads were the feature that made my projects feel real — the moment users can upload their own images, the app stops being a demo and starts being a product. Connect with me on LinkedIn or visit PrathamDEV.in. More articles on the way. Happy coding! 🚀 Written by Pratham Bhardwaj | Web Dev Cohort 2026, ChaiCode