DEV Community

Bhupesh Chandra Joshi
Bhupesh Chandra Joshi

Posted on

Handling File Uploads in Express.js with Multer: The Complete Guide (2026 Edition)

File uploads are a core feature in most modern web applications — whether it’s profile pictures, product images, documents, or CSV imports. Express.js doesn’t handle multipart/form-data out of the box, which is why we need Multer.

This is your one-stop, brain-friendly guide to mastering file uploads with Multer.


Why Do We Need Middleware for File Uploads?

HTML forms with enctype="multipart/form-data" send data differently from regular application/x-www-form-urlencoded forms.

  • Text fields → easy
  • Files → binary data + metadata

Node.js/Express req.body and req.query can’t parse this format natively. That’s where Multer comes in — it parses the multipart request, extracts files and fields, and attaches them to the request object (req.file / req.files).

Simple Analogy: Think of Multer as a helpful receptionist who opens the package (multipart request), sorts the documents (text fields) and gifts (files), and hands them to you in an organized way.


What is Multer?

Multer is a Node.js middleware for handling multipart/form-data. It’s built on top of busboy (very fast) and provides a clean API for:

  • Saving files to disk
  • Keeping files in memory
  • Filtering files by type/size
  • Renaming files
  • Handling multiple files

Official npm: https://www.npmjs.com/package/multer


Installation

# npm
npm install multer

# pnpm
pnpm add multer

# yarn
yarn add multer
Enter fullscreen mode Exit fullscreen mode

For TypeScript users:

# npm
npm install --save-dev @types/multer

# pnpm / yarn similarly
Enter fullscreen mode Exit fullscreen mode

Project Setup

mkdir multer-express-upload
cd multer-express-upload
npm init -y
npm install express multer
Enter fullscreen mode Exit fullscreen mode

Basic server.js:

const express = require('express');
const multer = require('multer');
const path = require('path');

const app = express();
const PORT = 3000;

// Serve static files (uploaded images)
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));

app.get('/', (req, res) => {
  res.sendFile(path.join(__dirname, 'index.html'));
});

app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));
Enter fullscreen mode Exit fullscreen mode

Storage Configuration Basics

Multer needs a storage engine.

1. Disk Storage (Most Common)

const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, 'uploads/');        // folder must exist
  },
  filename: function (req, file, cb) {
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
    cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
  }
});

const upload = multer({ storage: storage });
Enter fullscreen mode Exit fullscreen mode

2. Memory Storage (For Cloud Uploads)

const storage = multer.memoryStorage();
const upload = multer({ storage: storage });
Enter fullscreen mode Exit fullscreen mode

Tip: Use memoryStorage when uploading directly to AWS S3, Cloudinary, etc.


Handling Single File Upload

HTML Form (index.html):

<form action="/upload" method="POST" enctype="multipart/form-data">
  <input type="file" name="avatar" />
  <button type="submit">Upload</button>
</form>
Enter fullscreen mode Exit fullscreen mode

Route:

app.post('/upload', upload.single('avatar'), (req, res) => {
  if (!req.file) {
    return res.status(400).send('No file uploaded');
  }

  console.log(req.file);

  res.json({
    message: 'File uploaded successfully',
    file: {
      filename: req.file.filename,
      path: `/uploads/${req.file.filename}`,
      size: req.file.size,
      mimetype: req.file.mimetype
    }
  });
});
Enter fullscreen mode Exit fullscreen mode

req.file object contains:

  • fieldname, originalname, encoding, mimetype
  • destination, filename, path, size

Handling Multiple Files

Multiple files with same field name

app.post('/upload-multiple', upload.array('photos', 5), (req, res) => {  // max 5 files
  res.json({
    message: `${req.files.length} files uploaded`,
    files: req.files
  });
});
Enter fullscreen mode Exit fullscreen mode

Multiple fields with different names

const uploadFields = upload.fields([
  { name: 'avatar', maxCount: 1 },
  { name: 'documents', maxCount: 3 }
]);

app.post('/upload-fields', uploadFields, (req, res) => {
  res.json({
    avatar: req.files['avatar'],
    documents: req.files['documents']
  });
});
Enter fullscreen mode Exit fullscreen mode

File Filtering & Validation (Very Important)

const fileFilter = (req, file, cb) => {
  // Allowed extensions
  const allowedTypes = /jpeg|jpg|png|gif|pdf/;
  const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
  const mimetype = allowedTypes.test(file.mimetype);

  if (extname && mimetype) {
    return cb(null, true);
  } else {
    cb(new Error('Only images and PDFs are allowed!'));
  }
};

const upload = multer({
  storage: storage,
  limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
  fileFilter: fileFilter
});
Enter fullscreen mode Exit fullscreen mode

Handle errors gracefully:

app.post('/upload', (req, res, next) => {
  upload.single('avatar')(req, res, (err) => {
    if (err) {
      if (err.code === 'LIMIT_FILE_SIZE') {
        return res.status(400).send('File too large (max 5MB)');
      }
      return res.status(400).send(err.message);
    }
    next();
  });
}, (req, res) => {
  // success handler
});
Enter fullscreen mode Exit fullscreen mode

Multer Upload Lifecycle (Brain-Friendly Flow)

graph TD
    A[Client sends multipart/form-data] --> B[Multer Middleware]
    B --> C{Parse Request}
    C --> D[Apply fileFilter]
    D --> E{Check Limits}
    E --> F[Save to storage]
    F --> G[Attach to req.file / req.files]
    G --> H[Your Route Handler]
Enter fullscreen mode Exit fullscreen mode

Serving Uploaded Files

// Make sure this is before your routes
app.use('/uploads', express.static('uploads'));
Enter fullscreen mode Exit fullscreen mode

For production, use Nginx or a CDN instead of serving static files through Node.js.


Complete Example with Folder Creation

const fs = require('fs');

// Create uploads folder if not exists
const uploadDir = 'uploads';
if (!fs.existsSync(uploadDir)) {
  fs.mkdirSync(uploadDir);
}
Enter fullscreen mode Exit fullscreen mode

Best Practices & Pro Tips

  1. Always validate file types and sizes
  2. Rename files — never trust originalname (security + collision)
  3. Use UUID for filenames in production
  4. Scan files for malware (ClamAV) in critical apps
  5. Clean up temporary files on error (memory storage)
  6. Use environment variables for upload paths
  7. Rate limit upload endpoints
  8. Compress images before saving (sharp library)

Recommended Additional Packages:

npm install sharp uuid express-rate-limit
# pnpm add sharp uuid express-rate-limit
# yarn add sharp uuid express-rate-limit
Enter fullscreen mode Exit fullscreen mode

Common Pitfalls

  • Forgetting enctype="multipart/form-data"
  • Not creating the uploads directory
  • Using upload.single() with multiple files
  • Serving large files directly from Express in production
  • Not handling errors from Multer

Advanced: Upload to Cloud Storage

Once you’re comfortable with disk storage, move to:

  • AWS S3 + Multer-S3
  • Cloudinary
  • Google Cloud Storage

I’ll write separate deep-dive blogs for these.


Conclusion

You now have everything you need to handle file uploads professionally in Express.js using Multer — from basics to production-ready patterns.

Master Multer → you can build profile systems, e-commerce product uploads, document management, and more.

Happy Coding! 🚀


References & Further Reading

Other blogs in this Express Series:


Share this blog if it helped you! Drop your questions or your favorite upload tip in the comments.

Last updated: May 2026

Written for developers who want clean, secure, and scalable solutions.


Top comments (0)