Multer is the most popular and reliable middleware for handling file uploads in Node.js and Express.js applications. File uploads are one of those features that seem simple until you actually try to implement them securely. I've dealt with everything from users uploading 500MB files that crash the server to malicious files that could compromise the system.
When I first started working with file uploads in Express.js, I quickly learned that handling multipart/form-data isn't as straightforward as it appears. That's where Multer comes in. Multer is a middleware specifically designed for handling file uploads in Express.js and Node.js applications.
π Want the complete guide with more examples and advanced patterns? Check out the full article on my blog for an in-depth tutorial with additional code examples, troubleshooting tips, and real-world use cases.
What is Multer?
Multer is a Node.js middleware for handling multipart/form-data, which is primarily used for uploading files. Multer is the most popular solution for handling file uploads in Node.js and Express.js applications.
What is express multer? Express multer is the integration of multer middleware with Express.js. Express multer processes the multipart/form-data that browsers send when submitting forms with file inputs.
What is node multer? Node multer refers to using multer in Node.js applications. Multer works with any Node.js framework, not just Express.js.
Installation
Installing multer is straightforward:
npm install multer
For TypeScript projects, also install the types:
npm install --save-dev @types/multer @types/express
Basic Configuration
Setting up multer with disk storage is the most common configuration:
const multer = require("multer");
const path = require("path");
const fs = require("fs");
// Create uploads directory if it doesn't exist
const uploadDir = "uploads/";
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, uploadDir);
},
filename: function (req, file, cb) {
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 10000);
const ext = path.extname(file.originalname);
cb(null, file.fieldname + "-" + uniqueSuffix + ext);
},
});
const upload = multer({
storage: storage,
limits: {
fileSize: 5 * 1024 * 1024, // 5MB limit
},
fileFilter: (req, file, cb) => {
// Only allow images
if (file.mimetype.startsWith("image/")) {
cb(null, true);
} else {
cb(new Error("Only image files are allowed!"), false);
}
},
});
Single File Upload
Here's how to handle a single file upload:
const uploadSingleFile = upload.single("file");
function storeSingleFile(req, res, next) {
return new Promise((resolve, reject) => {
uploadSingleFile(req, res, async function (err) {
if (err instanceof multer.MulterError) {
reject(err);
} else if (err) {
reject(err);
}
resolve({
message: "File uploaded successfully",
file: req.file ? req.file : null,
});
});
});
}
// Usage in route
router.post("/upload", async (req, res, next) => {
try {
await storeSingleFile(req, res, next);
res.json({
success: true,
message: "File uploaded",
file: req.file,
});
} catch (error) {
res.status(400).json({
success: false,
message: error.message,
});
}
});
Multiple File Upload
Handling multiple files (like product images with main image and gallery):
// For products: main image (required) + gallery images (optional)
const uploadProductFiles = upload.fields([
{ name: "product_image", maxCount: 1 }, // Main product image
{ name: "product_gallery", maxCount: 10 }, // Gallery images (max 10)
]);
function storeProductFiles(req, res, next) {
return new Promise((resolve, reject) => {
uploadProductFiles(req, res, async function (err) {
if (err instanceof multer.MulterError) {
reject(err);
} else if (err) {
reject(err);
}
resolve({
message: "Files uploaded successfully",
files: req.files ? req.files : null,
});
});
});
}
// Usage in route
router.post("/products", async (req, res, next) => {
try {
await storeProductFiles(req, res, next);
// Access files
const mainImage = req.files?.product_image?.[0];
const galleryImages = req.files?.product_gallery || [];
res.json({
success: true,
message: "Product created",
files: {
mainImage: mainImage?.filename,
gallery: galleryImages.map(f => f.filename),
},
});
} catch (error) {
res.status(400).json({
success: false,
message: error.message,
});
}
});
File Validation
Adding proper file validation is crucial for security:
const upload = multer({
storage: storage,
limits: {
fileSize: 5 * 1024 * 1024, // 5MB
files: 10, // Max 10 files
},
fileFilter: (req, file, cb) => {
// Check file type
const allowedTypes = ["image/jpeg", "image/jpg", "image/png", "image/webp"];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error("Invalid file type. Only JPEG, PNG, and WebP are allowed!"), false);
}
},
});
Error Handling
Proper error handling is essential:
router.post("/upload", async (req, res) => {
try {
await storeProductFiles(req, res);
// Check if main image is required
if (!req.files?.product_image || req.files.product_image.length === 0) {
return res.status(400).json({
success: false,
message: "Product image is required",
});
}
// Process files...
} catch (error) {
if (error instanceof multer.MulterError) {
if (error.code === "LIMIT_FILE_SIZE") {
return res.status(400).json({
success: false,
message: "File size too large. Maximum size is 5MB",
});
}
if (error.code === "LIMIT_FILE_COUNT") {
return res.status(400).json({
success: false,
message: "Too many files. Maximum is 10 files",
});
}
}
return res.status(400).json({
success: false,
message: error.message || "File upload failed",
});
}
});
TypeScript Setup
When working with multer typescript, you need to properly type the multer file object:
import multer from "multer";
import { Request } from "express";
// Express multer file typescript type
interface MulterRequest extends Request {
file?: Express.Multer.File;
files?: {
[fieldname: string]: Express.Multer.File[];
} | Express.Multer.File[];
}
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, "uploads/");
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 10000);
cb(null, `${file.fieldname}-${uniqueSuffix}${path.extname(file.originalname)}`);
},
});
const upload = multer({
storage: storage,
limits: { fileSize: 5 * 1024 * 1024 },
});
// Express multer file typescript usage
router.post("/upload", upload.single("file"), (req: MulterRequest, res) => {
if (!req.file) {
return res.status(400).json({ error: "No file uploaded" });
}
// Access express multer file properties
const file: Express.Multer.File = req.file;
console.log("File type:", file.mimetype);
console.log("File size:", file.size);
res.json({
message: "File uploaded",
file: {
filename: file.filename,
originalname: file.originalname,
mimetype: file.mimetype,
size: file.size,
},
});
});
Memory Storage (File Buffer)
When using multer memoryStorage, files are stored in memory as Buffer objects instead of being saved to disk. This is useful when you need to process files (like uploading to cloud storage) without saving them locally:
const multer = require("multer");
// Express multer memoryStorage - files stored as buffer
const storage = multer.memoryStorage();
const upload = multer({
storage: storage,
limits: {
fileSize: 5 * 1024 * 1024, // 5MB
},
});
// Express multer file buffer example
router.post("/upload", upload.single("file"), async (req, res) => {
if (!req.file) {
return res.status(400).json({ error: "No file uploaded" });
}
// Express multer file buffer is available in req.file.buffer
const fileBuffer = req.file.buffer;
// Process the buffer (e.g., upload to cloud storage)
console.log("File buffer size:", fileBuffer.length);
console.log("File mimetype:", req.file.mimetype);
// Example: Convert express multer file buffer to base64
const base64String = fileBuffer.toString("base64");
res.json({
message: "File processed",
bufferSize: fileBuffer.length,
mimetype: req.file.mimetype,
});
});
Multer with Cloudinary and S3
Using express multer cloudinary or express multer s3 allows you to upload files directly to cloud storage:
const multer = require("multer");
const cloudinary = require("cloudinary").v2;
const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3");
// Express multer cloudinary example
const uploadToCloudinary = multer({ storage: multer.memoryStorage() });
router.post("/upload-cloudinary", uploadToCloudinary.single("file"), async (req, res) => {
if (!req.file) {
return res.status(400).json({ error: "No file uploaded" });
}
try {
// Upload express multer file buffer to Cloudinary
const result = await new Promise((resolve, reject) => {
const uploadStream = cloudinary.uploader.upload_stream(
{ folder: "uploads" },
(error, result) => {
if (error) reject(error);
else resolve(result);
}
);
uploadStream.end(req.file.buffer);
});
res.json({
message: "File uploaded to Cloudinary",
url: result.secure_url,
publicId: result.public_id,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Express multer s3 example
const uploadToS3 = multer({ storage: multer.memoryStorage() });
const s3Client = new S3Client({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
});
router.post("/upload-s3", uploadToS3.single("file"), async (req, res) => {
if (!req.file) {
return res.status(400).json({ error: "No file uploaded" });
}
try {
const key = `uploads/${Date.now()}-${req.file.originalname}`;
// Upload express multer file buffer to S3
const command = new PutObjectCommand({
Bucket: process.env.S3_BUCKET_NAME,
Key: key,
Body: req.file.buffer,
ContentType: req.file.mimetype,
});
await s3Client.send(command);
const fileUrl = `https://${process.env.S3_BUCKET_NAME}.s3.${process.env.AWS_REGION}.amazonaws.com/${key}`;
res.json({
message: "File uploaded to S3",
url: fileUrl,
key: key,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
Common Errors and Solutions
1. Express Multer File Not Found
Always check if req.file exists:
router.post("/upload", upload.single("file"), (req, res) => {
if (!req.file) {
return res.status(400).json({
error: "No file uploaded. Make sure form field name is 'file'"
});
}
// Process file...
});
2. Express Multer Unexpected End of Form
This usually means the request was interrupted or the file is too large:
// Solution: Increase body parser limits
const express = require("express");
const app = express();
app.use(express.json({ limit: "50mb" }));
app.use(express.urlencoded({ limit: "50mb", extended: true }));
// Also set multer limits
const upload = multer({
storage: storage,
limits: {
fileSize: 10 * 1024 * 1024, // 10MB
},
});
3. TypeScript Errors
Install types and use proper imports:
// Install: npm install --save-dev @types/multer @types/express
import multer from "multer";
import { Request } from "express";
interface MulterRequest extends Request {
file?: Express.Multer.File;
}
Multer vs express-fileupload
| Feature | Express Multer | express-fileupload |
|---|---|---|
| Popularity | More popular, widely used | Less popular |
| Storage Options | diskStorage, memoryStorage, custom | Memory only, manual disk save |
| TypeScript Support | Excellent | Limited |
| File Validation | Built-in fileFilter | Manual validation |
| Error Handling | MulterError with specific codes | Generic errors |
| Maintenance | Active maintenance | Less active |
Multer is generally preferred for production applications due to better TypeScript support, more storage options, and active maintenance.
Best Practices
- Always validate file types - Don't trust client-provided MIME types
- Set file size limits - Prevent DoS attacks from large files
- Sanitize filenames - Prevent path traversal attacks
- Use memoryStorage for cloud uploads - Process files before uploading to cloud storage
- Handle errors gracefully - Provide clear error messages to users
- Use TypeScript - For type safety in file upload handlers
- Implement rate limiting - Prevent abuse
- Store files outside web root - Prevent direct access
- Use cloud storage for production - Offload storage to CDN/cloud services
- Scan files for viruses - Use antivirus scanning in production
Production-Ready Example
Here's a complete production-ready configuration:
const multer = require("multer");
const path = require("path");
const fs = require("fs");
// Create uploads directory
const uploadDir = "uploads/";
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
// Validate file types strictly
const allowedMimeTypes = [
"image/jpeg",
"image/jpg",
"image/png",
"image/webp",
];
// Sanitize filenames
function sanitizeFilename(filename) {
return filename
.replace(/[^a-zA-Z0-9.-]/g, "_")
.replace(/\s+/g, "_")
.substring(0, 255);
}
const storage = multer.diskStorage({
destination: function (req, file, cb) {
const dateFolder = new Date().toISOString().split("T")[0];
const folderPath = path.join(uploadDir, dateFolder);
if (!fs.existsSync(folderPath)) {
fs.mkdirSync(folderPath, { recursive: true });
}
cb(null, folderPath);
},
filename: function (req, file, cb) {
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 10000);
const sanitized = sanitizeFilename(file.originalname);
const ext = path.extname(sanitized);
const name = path.basename(sanitized, ext);
cb(null, `${name}-${uniqueSuffix}${ext}`);
},
});
const upload = multer({
storage: storage,
limits: {
fileSize: 5 * 1024 * 1024, // 5MB
files: 10,
},
fileFilter: (req, file, cb) => {
if (allowedMimeTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error(`Invalid file type. Allowed types: ${allowedMimeTypes.join(", ")}`), false);
}
},
});
router.post("/upload", upload.single("file"), (req, res) => {
try {
if (!req.file) {
return res.status(400).json({
success: false,
message: "No file uploaded",
});
}
res.json({
success: true,
message: "File uploaded successfully",
file: {
filename: req.file.filename,
path: req.file.path,
size: req.file.size,
mimetype: req.file.mimetype,
},
});
} catch (error) {
res.status(500).json({
success: false,
message: "File upload failed",
error: error.message,
});
}
});
Resources and Further Reading
- π Full Multer File Upload Guide - Complete tutorial with advanced examples, troubleshooting, and best practices
- Multer GitHub - Official Multer repository
- Multer Documentation - Official documentation
- Express.js REST API Setup - Learn how to set up a production-ready Express.js REST API
- JWT Authentication in Express.js - Complete authentication guide
- Sequelize ORM with MySQL - Database setup guide
- Cloudinary Image Upload Guide - Cloud storage integration
Conclusion
Multer provides a robust and flexible solution for handling file uploads in Node.js and Express.js applications. Throughout this guide, we've covered everything from basic file uploads to advanced topics like TypeScript setup, file buffer handling, and cloud storage integration.
Key Takeaways:
- Multer is the industry standard for file uploads in Node.js
- Always validate file types and set size limits
- Use memoryStorage for cloud uploads, diskStorage for local storage
- Implement proper error handling and security measures
- Use TypeScript for type safety
- Consider cloud storage for production deployments
Whether you're building a simple file upload feature or a complex system with multiple files, cloud storage integration, or TypeScript support, this guide provides the foundation you need. Remember to always validate file types, sanitize filenames, and implement proper error handling for a secure and reliable file upload system.
What's your experience with Multer? Share your tips and tricks in the comments below! π
π‘ Looking for more details? This is a condensed version of my comprehensive guide. Read the full article on my blog for additional examples, advanced patterns, troubleshooting tips, NestJS integration, and more in-depth explanations.
If you found this guide helpful, consider checking out my other articles on Node.js development and backend development best practices.
Top comments (0)