Handling File Uploads in Express with Multer

File uploads are tricky. The browser sends files in a special format, your server needs to parse them, and you need to store them safely. Multer handles all of this automatically with just a few lines of code.
This is about understanding how file uploads work and building upload systems with Express and Multer.
Why File Uploads Need Middleware
When a user uploads a file, the browser doesn't send it like normal form data.
Regular Form Data
// Normal form data
const data = {
name: "Alice",
email: "alice@example.com"
};
// Sent as URL-encoded
name=Alice&email=alice@example.com
Express can parse this automatically with express.urlencoded().
File Uploads Are Different
Files are binary data. They can't be sent as simple text like name=Alice&email=alice@example.com.
The browser uses a special format called multipart/form-data.
What is Multipart/Form-Data?
Think of it like a package with multiple items:
------boundary123456
Content-Disposition: form-data; name="email"
alice@example.com
------boundary123456
Content-Disposition: form-data; name="file"; filename="resume.pdf"
Content-Type: application/pdf
[binary file data here]
------boundary123456--
It's a special format that separates form fields and files with boundaries.
Why You Need Middleware
Express by itself cannot:
- Parse multipart/form-data
- Extract files from the request
- Save files to disk
- Handle file streams safely
Without middleware, you'd have to:
// Without middleware: painful
app.post("/upload", (req, res) => {
// Manually parse multipart boundaries
// Manually extract binary data
// Manually save to disk
// Manually handle errors
// 100+ lines of code
});
With Multer:
// With Multer: simple
app.post("/upload", multer({ dest: "uploads/" }).single("file"), (req, res) => {
// File is already saved
res.json({ message: "File uploaded" });
});
That's why you need middleware for file uploads.
What is Multer?
Multer is Express middleware that handles multipart/form-data.
Simple Definition
Multer is a library that:
- Parses incoming file uploads
- Extracts files from requests
- Saves files to your server
- Provides file information to your route
It sits between the client and your route handler.
Real Example
Client sends file
|
v
Multer receives multipart/form-data
|
v
Multer parses the boundaries
|
v
Multer extracts the file
|
v
Multer saves to disk (or memory)
|
v
Your route handler gets req.file
|
v
You send response
Why Multer
Before Multer, developers had to:
- Manually parse multipart boundaries
- Handle file streams
- Validate file types
- Manage storage locations
Multer automates all of this.
Installation
npm install multer
Multer Middleware Execution Flow
Request arrives with file
|
v
Multer checks Content-Type
(must be multipart/form-data)
|
v
Multer parses boundaries
|
v
Multer extracts file(s)
|
v
Multer validates (size, type, etc)
|
v
Multer saves to destination
|
v
Multer populates req.file or req.files
|
v
Route handler executes
The middleware does all the heavy lifting before your code runs.
Handling Single File Upload
Basic Setup
const express = require("express");
const multer = require("multer");
const app = express();
// Configure multer
const upload = multer({ dest: "uploads/" });
// Single file upload route
app.post("/upload", upload.single("file"), (req, res) => {
// req.file contains the uploaded file
res.json({
message: "File uploaded successfully",
file: req.file
});
});
app.listen(3000, () => {
console.log("Server running on http://localhost:3000");
});
What Happens
- Client sends file in form field named "file"
- Multer intercepts the request
- Multer saves file to
uploads/directory - Multer adds
req.fileobject to the request - Route handler accesses
req.file
The req.file Object
app.post("/upload", upload.single("file"), (req, res) => {
console.log(req.file);
// Outputs:
// {
// fieldname: "file", // Form field name
// originalname: "resume.pdf", // Original filename
// encoding: "7bit", // File encoding
// mimetype: "application/pdf", // File type
// destination: "uploads/", // Where it was saved
// filename: "abc123def456", // Saved filename (random)
// path: "uploads/abc123def456", // Full path
// size: 45678 // File size in bytes
// }
res.json({
filename: req.file.filename,
size: req.file.size,
mimetype: req.file.mimetype
});
});
HTML Form for Upload
<!DOCTYPE html>
<html>
<body>
<form action="http://localhost:3000/upload" method="POST" enctype="multipart/form-data">
<input type="file" name="file" required>
<button type="submit">Upload</button>
</form>
</body>
</html>
Important: The enctype="multipart/form-data" is required. Without it, the file isn't sent correctly.
Test With curl
curl -X POST \
-F "file=@resume.pdf" \
http://localhost:3000/upload
The -F flag tells curl to send as multipart/form-data.
Handling Multiple File Uploads
Multiple Files, Single Field
Upload several files using the same form field:
const express = require("express");
const multer = require("multer");
const app = express();
const upload = multer({ dest: "uploads/" });
app.post("/upload-multiple", upload.array("files", 10), (req, res) => {
// req.files is an array of file objects
res.json({
message: "Files uploaded",
count: req.files.length,
files: req.files.map(f => ({
filename: f.filename,
size: f.size
}))
});
});
app.listen(3000);
What Changed
.single("file") becomes .array("files", 10).
The second argument is the maximum number of files allowed. This prevents abuse.
req.files Array
app.post("/upload-multiple", upload.array("files", 10), (req, res) => {
console.log(req.files);
// Outputs array of file objects:
// [
// { fieldname: "files", originalname: "doc1.pdf", ... },
// { fieldname: "files", originalname: "doc2.pdf", ... },
// { fieldname: "files", originalname: "doc3.pdf", ... }
// ]
req.files.forEach(file => {
console.log(`\({file.originalname} (\){file.size} bytes)`);
});
});
HTML Form for Multiple Files
<form action="http://localhost:3000/upload-multiple" method="POST" enctype="multipart/form-data">
<input type="file" name="files" multiple required>
<button type="submit">Upload Files</button>
</form>
The multiple attribute lets users select multiple files.
Different Fields, Different Files
Upload files to different form fields:
// .fields() takes an array of field configurations
const upload = multer({ dest: "uploads/" });
app.post("/upload-mixed", upload.fields([
{ name: "avatar", maxCount: 1 },
{ name: "documents", maxCount: 5 }
]), (req, res) => {
console.log(req.files);
// Outputs:
// {
// avatar: [ { file object } ],
// documents: [ { file object }, { file object }, ... ]
// }
const avatar = req.files.avatar[0];
const docs = req.files.documents;
res.json({ avatar, docs });
});
HTML Form with Multiple Fields
<form action="http://localhost:3000/upload-mixed" method="POST" enctype="multipart/form-data">
<label>Profile Picture</label>
<input type="file" name="avatar" required>
<label>Documents</label>
<input type="file" name="documents" multiple required>
<button type="submit">Upload</button>
</form>
Storage Configuration Basics
By default, Multer saves files with random names. You might want to customize this.
Memory Storage
Store files in RAM instead of disk:
const multer = require("multer");
// Memory storage (files in RAM)
const storage = multer.memoryStorage();
const upload = multer({ storage });
app.post("/upload", upload.single("file"), (req, res) => {
// File is in memory
console.log(req.file);
// {
// fieldname: "file",
// originalname: "resume.pdf",
// encoding: "7bit",
// mimetype: "application/pdf",
// buffer: <Buffer...>, // File binary data
// size: 45678
// }
// No destination or path (it's in RAM)
res.json({ message: "File received" });
});
Use memory storage when:
- Files are small
- You process them immediately
- You don't need to persist them
Don't use memory storage when:
- Files are large (drains RAM)
- Many users upload simultaneously
- You need to store files permanently
Disk Storage with Custom Names
Save to disk with meaningful filenames:
const multer = require("multer");
const path = require("path");
// Disk storage with custom configuration
const storage = multer.diskStorage({
destination: (req, file, cb) => {
// Where to save
cb(null, "uploads/");
},
filename: (req, file, cb) => {
// How to name the file
// cb(null, originalname) keeps the original name
// But this is risky (filename injection)
// Better: use timestamp + original extension
const uniqueName = Date.now() + path.extname(file.originalname);
cb(null, uniqueName);
}
});
const upload = multer({ storage });
app.post("/upload", upload.single("file"), (req, res) => {
console.log(req.file.filename);
// Output: 1620000000000.pdf
res.json({ filename: req.file.filename });
});
Safe Filename Strategy
Never trust the user's filename. Always generate your own:
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, "uploads/");
},
filename: (req, file, cb) => {
// Option 1: Timestamp + extension
const filename = Date.now() + path.extname(file.originalname);
cb(null, filename);
// Option 2: UUID + extension
// const filename = uuid() + path.extname(file.originalname);
// cb(null, filename);
// Option 3: User ID + timestamp
// const filename = req.user.id + "-" + Date.now() + path.extname(file.originalname);
// cb(null, filename);
}
});
Always append the file extension to preserve file type.
Complete Storage Example
const express = require("express");
const multer = require("multer");
const path = require("path");
const app = express();
// Configure storage
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, "uploads/");
},
filename: (req, file, cb) => {
const uniqueName = Date.now() + path.extname(file.originalname);
cb(null, uniqueName);
}
});
const upload = multer({
storage,
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB max
fileFilter: (req, file, cb) => {
// Only allow PDF and images
const allowedMimes = ["application/pdf", "image/jpeg", "image/png"];
if (allowedMimes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error("Invalid file type"));
}
}
});
app.post("/upload", upload.single("file"), (req, res) => {
res.json({
message: "File uploaded",
filename: req.file.filename,
size: req.file.size
});
});
app.listen(3000);
Key options:
limits.fileSize: Maximum file size in bytesfileFilter: Validate file type, name, or size
Serving Uploaded Files
After uploading, users need to download or view files.
Static File Serving
Tell Express to serve files from the uploads directory:
const express = require("express");
const multer = require("multer");
const app = express();
// Serve uploaded files as static
app.use("/uploads", express.static("uploads/"));
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, "uploads/");
},
filename: (req, file, cb) => {
cb(null, Date.now() + path.extname(file.originalname));
}
});
const upload = multer({ storage });
app.post("/upload", upload.single("file"), (req, res) => {
const fileUrl = `/uploads/${req.file.filename}`;
res.json({
message: "File uploaded",
url: fileUrl // Client can access at this URL
});
});
app.listen(3000);
Now files are accessible at http://localhost:3000/uploads/filename.
Downloading vs Viewing
In the browser, images display. But PDFs should download:
app.get("/download/:filename", (req, res) => {
const filename = req.params.filename;
const filepath = path.join(__dirname, "uploads", filename);
// Force download
res.download(filepath);
});
The .download() method tells the browser to download instead of display.
Complete Upload and Download Example
const express = require("express");
const multer = require("multer");
const path = require("path");
const app = express();
// Serve uploads folder
app.use("/uploads", express.static("uploads/"));
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, "uploads/");
},
filename: (req, file, cb) => {
cb(null, Date.now() + path.extname(file.originalname));
}
});
const upload = multer({ storage });
// Upload endpoint
app.post("/upload", upload.single("file"), (req, res) => {
res.json({
message: "File uploaded",
filename: req.file.filename,
url: `/uploads/${req.file.filename}`
});
});
// Download endpoint
app.get("/download/:filename", (req, res) => {
const filepath = path.join(__dirname, "uploads", req.params.filename);
res.download(filepath);
});
// View files endpoint
app.get("/files", (req, res) => {
const fs = require("fs");
const files = fs.readdirSync("uploads/");
res.json({ files });
});
app.listen(3000, () => {
console.log("Server running on http://localhost:3000");
});
Test Upload and Download
Upload:
curl -X POST \
-F "file=@resume.pdf" \
http://localhost:3000/upload
Response:
{
"message": "File uploaded",
"filename": "1620000000000.pdf",
"url": "/uploads/1620000000000.pdf"
}
Download:
curl -O http://localhost:3000/download/1620000000000.pdf
Or in the browser: http://localhost:3000/uploads/1620000000000.pdf
Client to Server to Storage Upload Lifecycle
User selects file in browser
|
v
Form sends POST request
(multipart/form-data format)
|
v
Express receives request
|
v
Multer middleware intercepts
|
v
Multer parses multipart boundaries
|
v
Multer extracts file data
|
v
Multer validates file
(type, size, filters)
|
├─ Invalid → Error response
└─ Valid → Continue
|
v
Multer saves to disk
(or memory)
|
v
Multer populates req.file
|
v
Route handler executes
|
v
Handler sends response
(with file info/URL)
|
v
Browser receives response
|
v
User can access file at URL
Common Multer Options
File Size Limits
const upload = multer({
storage,
limits: {
fileSize: 5 * 1024 * 1024 // 5MB
}
});
Reject files larger than 5MB.
File Type Validation
const upload = multer({
storage,
fileFilter: (req, file, cb) => {
if (file.mimetype === "application/pdf") {
cb(null, true); // Accept
} else {
cb(new Error("Only PDF files allowed")); // Reject
}
}
});
Only allow PDFs. Reject other types.
Multiple Extensions
const upload = multer({
storage,
fileFilter: (req, file, cb) => {
const allowedTypes = ["image/jpeg", "image/png", "application/pdf"];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error("Invalid file type"));
}
}
});
Allow images and PDFs.
Error Handling
app.post("/upload", (req, res) => {
upload.single("file")(req, res, (err) => {
if (err instanceof multer.MulterError) {
// Multer error (file too large, etc)
return res.status(400).json({ error: err.message });
} else if (err) {
// Custom error (from fileFilter)
return res.status(400).json({ error: err.message });
}
// Success
res.json({ message: "File uploaded", file: req.file });
});
});
Catch both Multer errors and custom validation errors.
Complete Practical Example
const express = require("express");
const multer = require("multer");
const path = require("path");
const fs = require("fs");
const app = express();
app.use(express.json());
// Create uploads folder if it doesn't exist
if (!fs.existsSync("uploads/")) {
fs.mkdirSync("uploads/");
}
// Serve uploaded files
app.use("/uploads", express.static("uploads/"));
// Configure storage
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, "uploads/");
},
filename: (req, file, cb) => {
const uniqueName = Date.now() + path.extname(file.originalname);
cb(null, uniqueName);
}
});
// Configure upload with validation
const upload = multer({
storage,
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB max
fileFilter: (req, file, cb) => {
const allowedTypes = ["image/jpeg", "image/png", "application/pdf"];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error("Only JPEG, PNG, and PDF files are allowed"));
}
}
});
// Single file upload
app.post("/upload", (req, res) => {
upload.single("file")(req, res, (err) => {
if (err) {
return res.status(400).json({ error: err.message });
}
if (!req.file) {
return res.status(400).json({ error: "No file provided" });
}
res.json({
message: "File uploaded successfully",
file: {
filename: req.file.filename,
originalname: req.file.originalname,
size: req.file.size,
url: `/uploads/${req.file.filename}`
}
});
});
});
// Multiple files upload
app.post("/upload-multiple", (req, res) => {
upload.array("files", 10)(req, res, (err) => {
if (err) {
return res.status(400).json({ error: err.message });
}
if (!req.files || req.files.length === 0) {
return res.status(400).json({ error: "No files provided" });
}
const files = req.files.map(f => ({
filename: f.filename,
originalname: f.originalname,
size: f.size,
url: `/uploads/${f.filename}`
}));
res.json({
message: "Files uploaded successfully",
count: files.length,
files
});
});
});
// List uploaded files
app.get("/files", (req, res) => {
fs.readdir("uploads/", (err, files) => {
if (err) {
return res.status(500).json({ error: "Could not read files" });
}
res.json({ files });
});
});
// Download file
app.get("/download/:filename", (req, res) => {
const filepath = path.join(__dirname, "uploads", req.params.filename);
// Prevent directory traversal attacks
if (!filepath.startsWith(path.join(__dirname, "uploads"))) {
return res.status(403).json({ error: "Forbidden" });
}
res.download(filepath);
});
app.listen(3000, () => {
console.log("Server running on http://localhost:3000");
});
Test it:
# Upload single file
curl -X POST \
-F "file=@resume.pdf" \
http://localhost:3000/upload
# Upload multiple files
curl -X POST \
-F "files=@file1.pdf" \
-F "files=@file2.pdf" \
http://localhost:3000/upload-multiple
# List files
curl http://localhost:3000/files
# Download file
curl -O http://localhost:3000/download/1620000000000.pdf
Practice Assignment
1. Build a single file upload route:
// Create POST /upload endpoint
// Accept file in "file" form field
// Save to "uploads/" directory
// Return uploaded file info
// Test with curl
2. Add file validation:
// Validate file type (only PDF and images)
// Validate file size (max 2MB)
// Return error if validation fails
// Test with invalid file
3. Create multiple file upload:
// Create POST /upload-multiple endpoint
// Accept up to 5 files in "files" field
// Save all files
// Return array of file info
// Test with multiple files
4. Serve uploaded files:
// Use express.static() for uploads folder
// Create GET /files endpoint to list all files
// Create GET /download/:filename endpoint
// Test accessing and downloading files
5. Add error handling:
// Handle Multer errors (file too large)
// Handle custom validation errors
// Return meaningful error messages
// Test with various error scenarios
Quick Recap
File uploads are sent as multipart/form-data, a special format the browser uses to send binary data.
Middleware is needed because Express doesn't parse multipart/form-data by default.
Multer is Express middleware that parses multipart/form-data and saves files.
.single("field")uploads one file from form field "field"..array("field", max)uploads multiple files from the same field..fields()uploads different files to different fields.req.file contains info about a single uploaded file (with
.single()).req.files contains info about multiple uploaded files (with
.array()or.fields()).Memory storage stores files in RAM (good for small, temporary files).
Disk storage saves files to the server filesystem (good for persistent storage).
Always use custom filenames to prevent filename injection attacks.
File validation with
fileFilterprevents unwanted file types.File size limits prevent storage abuse.
express.static()serves uploaded files so users can access them.res.download()forces download instead of viewing in the browser.Multipart/form-data requires the HTML form attribute
enctype="multipart/form-data".
Multer makes file uploads simple and secure.
If you enjoyed this article, check out my other blogs on this profile. Connect with me: LinkedIn | GitHub | X (Twitter)




