Skip to main content

Command Palette

Search for a command to run...

Handling File Uploads in Express with Multer

Updated
15 min read
Handling File Uploads in Express with Multer
H
CS undergrad | Tech enthusiast | Focusing on Web Dev • DSA • ML | Building skills for real-world impact

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

  1. Client sends file in form field named "file"
  2. Multer intercepts the request
  3. Multer saves file to uploads/ directory
  4. Multer adds req.file object to the request
  5. 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 bytes
  • fileFilter: 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 fileFilter prevents 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)