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.
Why File Uploads Need Middleware
When you send JSON data to Express, the body looks like this:
Content-Type: application/json
{"name": "Pratham", "email": "pratham@dev.in"}
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
What Is 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).
Install
npm install multer
What Multer Does
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.
Handling Single File Upload
Let's start with the most common scenario: uploading one file — like a profile picture.
Basic Setup
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"));
Key Points
-
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
}
Testing with curl
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.
Testing with an HTML Form
<form action="/upload/avatar" method="POST" enctype="multipart/form-data">
<input type="text" name="name" placeholder="Your name" />
<input type="file" name="avatar" />
<button type="submit">Upload</button>
</form>
The enctype="multipart/form-data" attribute is essential — without it, the browser won't send the file as binary data.
Client → Server → Storage Upload Flow
┌────────────────┐
│ Client │
│ │
│ <form> │
│ 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) │
└───────────────────┘
Handling Multiple File Uploads
Multiple Files — Same Field
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.filesis an array of file objects (same shape asreq.file)
Multiple Files — Different Fields
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,
});
});
HTML Form for Multiple Files
<!-- Same field (gallery) -->
<input type="file" name="photos" multiple />
<!-- Different fields -->
<input type="file" name="avatar" />
<input type="file" name="resume" />
Upload Methods Summary
| 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 |
Storage Configuration
By default, multer({ dest: "uploads/" }) saves files with random names and no extension. For more control, use Multer's disk storage engine.
Custom Storage
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.
Adding File Validation
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
}
},
});
Handling Upload Errors
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 });
});
});
Serving Uploaded Files
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
Complete Upload + Serve Example
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(`
<h1>File Upload</h1>
<form action="/api/upload" method="POST" enctype="multipart/form-data">
<input type="file" name="image" accept="image/*" />
<button type="submit">Upload</button>
</form>
`);
});
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.
Multer Middleware Execution Flow
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 │
│ │
└───────────────────────────────────────────────┘
Let's Practice: Hands-On Assignment
Part 1: Single File Upload
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
Part 2: Multiple File Upload
Add a route POST /upload/gallery that:
- Accepts up to 5 images
- Returns info about all uploaded files
Part 3: Add Validation
Modify your upload to:
- Only accept images (JPEG, PNG, GIF, WebP)
- Limit file size to 2 MB
- Return clear error messages when validation fails
Part 4: Serve and Display
- Serve the
uploads/folder withexpress.static() - Create a
GET /galleryroute that returns HTML showing all uploaded images
Key Takeaways
-
Files need special handling because they're sent as
multipart/form-data— a format thatexpress.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 withlimits, and handle errors gracefully. -
Serve uploads with
express.static("uploads")to make files accessible via URL.
Wrapping Up
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
Top comments (0)