Handle file uploads in Express with Multer — store images, validate file types, and manage upload limits.
When you build a REST API that accepts JSON data, you use express.json() middleware and the client sends a Content-Type: application/json header. This works perfectly for text-based data like names, emails, and numbers. But what happens when a user wants to upload a profile picture, a PDF resume, or a gallery of photos? JSON cannot carry binary file data. You cannot stringify an image and send it as a JSON field — it would be enormous, slow, and technically broken.
Files are sent using a different encoding called multipart/form-data. This is a special content type that allows a single HTTP request to carry both text fields and binary file data, separated by a unique boundary string. When you create an HTML form with <form enctype="multipart/form-data"> or use JavaScript's FormData API, the browser automatically formats the request body in this multipart format.
Here is what a multipart request looks like under the hood:
POST /api/upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="username"
alice
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="avatar"; filename="photo.jpg"
Content-Type: image/jpeg
[binary data here]
------WebKitFormBoundary7MA4YWxkTrZu0gW--Express's built-in express.json() and express.urlencoded() middleware cannot parse this format. They are designed for JSON and URL-encoded form data respectively. Attempting to read req.body on a multipart request will give you undefined or an empty object. You need dedicated middleware to handle multipart data.
Multer is the de facto standard middleware for handling multipart/form-data in Express. It is built on top of busboy, a fast streaming parser for multipart data. Multer processes the incoming file stream, stores it according to your configuration, and makes the file information available on req.file (for single uploads) or req.files (for multiple uploads). It also parses any text fields in the multipart request and puts them on req.body, just like express.json() does for JSON requests. Without Multer (or a similar library), handling file uploads in Express would require manually parsing the raw multipart stream — a complex and error-prone task.
Install Multer with npm install multer. Multer provides two built-in storage engines that determine where and how uploaded files are stored.
Disk Storage saves files directly to your server's filesystem. You configure a destination folder and a filename function. The destination is where files will be saved, and the filename function lets you rename files to avoid collisions — two users uploading photo.jpg would overwrite each other without unique names. The standard approach is to prepend a timestamp or UUID:
const multer = require('multer');
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/'); // folder must exist
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const ext = file.originalname.split('.').pop();
cb(null, file.fieldname + '-' + uniqueSuffix + '.' + ext);
}
});Memory Storage keeps files in memory as Buffer objects instead of writing them to disk. This is useful when you plan to immediately process the file (resize an image, upload to cloud storage) without saving it locally. Be cautious with memory storage — large files or many concurrent uploads can exhaust your server's RAM.
const storage = multer.memoryStorage();
// file available as req.file.bufferFile Filter lets you accept or reject files based on their MIME type. This is your first line of defense against malicious uploads:
const fileFilter = (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true); // accept
} else {
cb(new Error('Invalid file type. Only JPEG, PNG, and GIF allowed.'), false);
}
};Size Limits prevent users from uploading enormous files that could fill your disk or crash your server. Multer's limits option accepts a fileSize value in bytes:
const upload = multer({
storage,
limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB max
fileFilter
});The limits object can also control files (max number of files), fields (max number of non-file fields), fieldNameSize (max field name length), and fieldSize (max field value length). Setting these limits protects your server from denial-of-service attacks where an attacker sends extremely large or numerous fields to exhaust resources.
const express = require('express');
const multer = require('multer');
const path = require('path');
const app = express();
// ── Storage Configuration ────────────────────────────
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/'); // Files saved to /uploads directory
},
filename: (req, file, cb) => {
// Generate unique filename: avatar-1700000000000-123456789.jpg
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const ext = path.extname(file.originalname); // .jpg, .png, etc.
cb(null, file.fieldname + '-' + uniqueSuffix + ext);
}
});
// ── File Filter — Accept Images Only ─────────────────
const fileFilter = (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true); // Accept the file
} else {
cb(new Error('Only image files (JPEG, PNG, GIF, WebP) are allowed'), false);
}
};
// ── Create Multer Instance ───────────────────────────
const upload = multer({
storage,
limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB max
fileFilter
});
// ── Single File Upload Route ─────────────────────────
app.post('/api/upload/avatar', upload.single('avatar'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
res.json({
message: 'File uploaded successfully',
file: {
originalName: req.file.originalname,
filename: req.file.filename,
size: req.file.size,
mimetype: req.file.mimetype,
url: '/uploads/' + req.file.filename
}
});
});
// ── Error Handling for Multer ────────────────────────
app.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
// Multer-specific error (file too large, too many files, etc.)
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({ error: 'File is too large. Max size: 5MB' });
}
return res.status(400).json({ error: err.message });
}
if (err) {
// Custom error from fileFilter
return res.status(400).json({ error: err.message });
}
next();
});Multer provides four methods for different upload scenarios, each determining how many files the route accepts and how they are organized on the request object.
upload.single('fieldname') handles exactly one file. The field name must match the name attribute in the HTML form or the key in the FormData object. The uploaded file is available on req.file as an object containing originalname, filename, size, mimetype, path, and other metadata. Use this for profile pictures, document uploads, or any route that accepts one file.
app.post('/avatar', upload.single('avatar'), (req, res) => {
console.log(req.file); // Single file object
console.log(req.body); // Any text fields from the form
});upload.array('fieldname', maxCount) handles multiple files all under the same field name. The second argument limits how many files are accepted. The files are available on req.files as an array. Use this for photo galleries, batch document uploads, or any route where the user selects multiple files from a single file input.
app.post('/gallery', upload.array('photos', 10), (req, res) => {
console.log(req.files); // Array of file objects (up to 10)
console.log(req.files.length); // How many were uploaded
});upload.fields([...]) handles multiple files across different field names. This is the most flexible option. You pass an array of objects, each specifying a name and optional maxCount. The files are available on req.files as an object where each key is a field name mapping to an array of files. Use this when a single form has different types of file inputs — for example, a product listing with a main image and additional gallery photos.
app.post('/product', upload.fields([
{ name: 'mainImage', maxCount: 1 },
{ name: 'gallery', maxCount: 8 }
]), (req, res) => {
console.log(req.files.mainImage); // Array with 1 file
console.log(req.files.gallery); // Array with up to 8 files
});upload.none() accepts only text fields — no files. It parses the text fields from a multipart form and rejects any file uploads. Use this when a form uses multipart/form-data encoding but only contains text inputs, or when you want to explicitly disallow files on a specific route.
app.post('/profile', upload.none(), (req, res) => {
console.log(req.body); // Text fields only
});Choose the method that matches your use case. Using upload.single() when the client sends multiple files will silently ignore the extras. Using upload.array() when the client sends one file works fine — you just get an array with one element.
File uploads are one of the most dangerous features in any web application. A misconfigured upload endpoint can lead to remote code execution, denial of service, or data breaches. Follow these production-hardened practices to keep your application secure and performant.
Always validate file type — twice. Check both the MIME type and the file extension. MIME types can be spoofed by changing the Content-Type header, so an attacker could upload a .exe file with a image/jpeg MIME type. Validate the extension from the original filename AND check the MIME type. For maximum security, use a library like file-type that reads the file's magic bytes (the first few bytes of a file that identify its format) to verify the actual file type regardless of what the client claims.
Set strict size limits. Without limits, a single malicious user can upload a 10GB file and fill your disk. Set fileSize in Multer's limits configuration. Also set files to limit the number of files per request. Consider different limits for different routes — avatar uploads might allow 2MB while document uploads allow 10MB.
Generate unique filenames. Never use the original filename from the client. Two reasons: (1) two users uploading photo.jpg would overwrite each other, and (2) filenames can contain path traversal attacks like ../../../etc/passwd. Use a UUID or timestamp-based naming scheme: ${uuidv4()}${path.extname(originalname)}.
Store files outside your application directory. If uploaded files are stored inside your project (e.g., /src/uploads/), a malicious upload could potentially overwrite your application code. Use a dedicated uploads directory outside the project, or better yet, use cloud storage.
For production: use cloud storage. Local disk storage does not scale. If you have multiple server instances behind a load balancer, a file uploaded to Server A is not available on Server B. Cloud storage services solve this:
@aws-sdk/client-s3 package with Multer's multer-s3 storage engine.Serve uploaded files via a CDN. Do not serve uploads directly from your Express server — it wastes your server's CPU and bandwidth on static file delivery. Upload to S3, put CloudFront (AWS CDN) in front of it, and serve images from a URL like https://cdn.yourapp.com/uploads/avatar-123.jpg. This reduces server load and delivers files faster to users worldwide.
Clean up orphaned files. When a user deletes their account or changes their avatar, delete the old file from storage. Orphaned files accumulate over time and waste storage space and money. Implement a cleanup job or delete files as part of the update/delete logic.
What encoding type must HTML forms use to upload files?