DEV Community

Cover image for Multer File Upload in Express.js: Complete Guide for 2026
Md. Maruf Rahman
Md. Maruf Rahman

Posted on • Originally published at marufrahman.live

Multer File Upload in Express.js: Complete Guide for 2026

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
Enter fullscreen mode Exit fullscreen mode

For TypeScript projects, also install the types:

npm install --save-dev @types/multer @types/express
Enter fullscreen mode Exit fullscreen mode

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);
    }
  },
});
Enter fullscreen mode Exit fullscreen mode

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,
    });
  }
});
Enter fullscreen mode Exit fullscreen mode

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,
    });
  }
});
Enter fullscreen mode Exit fullscreen mode

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);
    }
  },
});
Enter fullscreen mode Exit fullscreen mode

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",
    });
  }
});
Enter fullscreen mode Exit fullscreen mode

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,
    },
  });
});
Enter fullscreen mode Exit fullscreen mode

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,
  });
});
Enter fullscreen mode Exit fullscreen mode

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 });
  }
});
Enter fullscreen mode Exit fullscreen mode

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...
});
Enter fullscreen mode Exit fullscreen mode

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
  },
});
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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

  1. Always validate file types - Don't trust client-provided MIME types
  2. Set file size limits - Prevent DoS attacks from large files
  3. Sanitize filenames - Prevent path traversal attacks
  4. Use memoryStorage for cloud uploads - Process files before uploading to cloud storage
  5. Handle errors gracefully - Provide clear error messages to users
  6. Use TypeScript - For type safety in file upload handlers
  7. Implement rate limiting - Prevent abuse
  8. Store files outside web root - Prevent direct access
  9. Use cloud storage for production - Offload storage to CDN/cloud services
  10. 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,
    });
  }
});
Enter fullscreen mode Exit fullscreen mode

Resources and Further Reading

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)