DEV Community

Cover image for Handling File Uploads in Express with Multer
Pratham
Pratham

Posted on

Handling File Uploads in Express with Multer

How to accept profile pictures, documents, and any file your users throw at your server.


The first time I tried to handle a file upload in Express, I wrote something like this:

app.post("/upload", (req, res) => {
  console.log(req.body); // Where's my file?
});
Enter fullscreen mode Exit fullscreen mode

I submitted a form with an image attached, checked req.body, and got... nothing. An empty object. The file had vanished into the void.

Turns out, express.json() and express.urlencoded() can't handle files. Files are sent in a completely different format called multipart/form-data, and Express has no built-in way to parse it. You need a middleware specifically designed for file uploads.

That middleware is Multer — and once I set it up in the ChaiCode Web Dev Cohort 2026, file uploads went from mysterious to straightforward. Let me show you.


Why File Uploads Need Middleware

When you send JSON data to Express, the body looks like this:

Content-Type: application/json

{"name": "Pratham", "email": "pratham@dev.in"}
Enter fullscreen mode Exit fullscreen mode

express.json() knows how to parse this. Simple text, simple format.

But when you send a file, the body looks completely different:

Content-Type: multipart/form-data; boundary=----abc123

------abc123
Content-Disposition: form-data; name="name"

Pratham
------abc123
Content-Disposition: form-data; name="avatar"; filename="photo.jpg"
Content-Type: image/jpeg

(binary image data — thousands of bytes of raw binary)
------abc123--
Enter fullscreen mode Exit fullscreen mode

This is multipart/form-data — a format that can carry both text fields and binary file data in the same request. It splits the body into parts (separated by a boundary string), each with its own headers and content.

express.json() can't parse this. It sees binary data and has no idea what to do with it. You need a dedicated multipart parser — and that's Multer.

Form Data Types:

  application/json
  → Text only. Parsed by express.json()

  application/x-www-form-urlencoded
  → Text only (form fields). Parsed by express.urlencoded()

  multipart/form-data
  → Text AND binary files. Parsed by MULTER
Enter fullscreen mode Exit fullscreen mode

What Is Multer?

Multer is a Node.js middleware for handling multipart/form-data. It processes file uploads and makes them available on req.file (single file) or req.files (multiple files).

Install

npm install multer
Enter fullscreen mode Exit fullscreen mode

What Multer Does

Without Multer:
  req.body  {}          (empty  file data is lost)
  req.file  undefined

With Multer:
  req.body  { name: "Pratham" }   (text fields parsed)
  req.file  {
    fieldname: "avatar",
    originalname: "photo.jpg",
    mimetype: "image/jpeg",
    size: 245678,
    path: "uploads/abc123-photo.jpg"
  }
Enter fullscreen mode Exit fullscreen mode

Multer parses the multipart request, extracts the file, saves it to disk (or keeps it in memory), and gives you all the file information on req.file.


Handling Single File Upload

Let's start with the most common scenario: uploading one file — like a profile picture.

Basic Setup

const express = require("express");
const multer = require("multer");

const app = express();
app.use(express.json());

// Configure multer — save files to "uploads/" folder
const upload = multer({ dest: "uploads/" });

// Single file upload route
app.post("/upload/avatar", upload.single("avatar"), (req, res) => {
  console.log("File:", req.file);
  console.log("Body:", req.body);

  res.json({
    message: "File uploaded successfully!",
    file: {
      originalName: req.file.originalname,
      size: req.file.size,
      path: req.file.path,
    },
  });
});

app.listen(3000, () => console.log("Server on http://localhost:3000"));
Enter fullscreen mode Exit fullscreen mode

Key Points

  • multer({ dest: "uploads/" }) — tells Multer where to save files
  • upload.single("avatar") — expects ONE file in a field called "avatar"
  • After processing, file info is on req.file

The req.file Object

{
  fieldname: "avatar",        // Name of the form field
  originalname: "photo.jpg",  // Original filename from the user
  encoding: "7bit",           // File encoding
  mimetype: "image/jpeg",     // File type
  destination: "uploads/",    // Where it was saved
  filename: "a1b2c3d4e5f6",   // Generated filename (no extension!)
  path: "uploads/a1b2c3d4e5f6", // Full path to saved file
  size: 245678                // File size in bytes
}
Enter fullscreen mode Exit fullscreen mode

Testing with curl

curl -X POST http://localhost:3000/upload/avatar \
  -F "avatar=@/path/to/photo.jpg" \
  -F "name=Pratham"
Enter fullscreen mode Exit fullscreen mode

-F sends form data in multipart format. @ reads from a file.

Testing with an HTML Form

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

The enctype="multipart/form-data" attribute is essential — without it, the browser won't send the file as binary data.


Client → Server → Storage Upload Flow

┌────────────────┐
│     Client     │
│                │
│  <form>        │
│  enctype=      │
│  "multipart/   │
│   form-data"   │
│                │
│  [photo.jpg]   │
│  [name field]  │
└───────┬────────┘
        │
        │  POST /upload/avatar
        │  Content-Type: multipart/form-data
        ↓
┌────────────────────────────────────────────┐
│              EXPRESS SERVER                  │
│                                            │
│  1. Request arrives                        │
│                                            │
│  2. Multer middleware runs:                │
│     → Parses multipart body               │
│     → Extracts file binary data            │
│     → Saves file to disk (uploads/)        │
│     → Populates req.file with metadata     │
│     → Populates req.body with text fields  │
│                                            │
│  3. Route handler runs:                    │
│     → Reads req.file and req.body          │
│     → Sends response                      │
│                                            │
└───────────────────┬────────────────────────┘
                    │
                    ↓
        ┌───────────────────┐
        │   uploads/ folder  │
        │                   │
        │  a1b2c3d4e5f6     │  ← saved file
        │  (photo.jpg data) │
        └───────────────────┘
Enter fullscreen mode Exit fullscreen mode

Handling Multiple File Uploads

Multiple Files — Same Field

For uploading multiple images at once (like a photo gallery):

// Accept up to 5 files from the "photos" field
app.post("/upload/gallery", upload.array("photos", 5), (req, res) => {
  console.log("Files:", req.files); // Array of file objects

  res.json({
    message: `${req.files.length} files uploaded!`,
    files: req.files.map((f) => ({
      name: f.originalname,
      size: f.size,
    })),
  });
});
Enter fullscreen mode Exit fullscreen mode
  • upload.array("photos", 5) — accepts up to 5 files from the "photos" field
  • req.files is an array of file objects (same shape as req.file)

Multiple Files — Different Fields

For forms with separate file inputs (avatar + resume):

const multiUpload = upload.fields([
  { name: "avatar", maxCount: 1 },
  { name: "resume", maxCount: 1 },
]);

app.post("/upload/profile", multiUpload, (req, res) => {
  console.log("Avatar:", req.files["avatar"][0]);
  console.log("Resume:", req.files["resume"][0]);

  res.json({
    avatar: req.files["avatar"][0].originalname,
    resume: req.files["resume"][0].originalname,
  });
});
Enter fullscreen mode Exit fullscreen mode

HTML Form for Multiple Files

<!-- Same field (gallery) -->
<input type="file" name="photos" multiple />

<!-- Different fields -->
<input type="file" name="avatar" />
<input type="file" name="resume" />
Enter fullscreen mode Exit fullscreen mode

Upload Methods Summary

Method Use Case Access Files Via
upload.single("fieldName") One file req.file
upload.array("fieldName", max) Multiple files, same field req.files (array)
upload.fields([{ name, maxCount }]) Multiple files, different fields req.files["name"]
upload.none() No files (text fields only) req.body

Storage Configuration

By default, multer({ dest: "uploads/" }) saves files with random names and no extension. For more control, use Multer's disk storage engine.

Custom Storage

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, "uploads/"); // Save to uploads/ folder
  },
  filename: (req, file, cb) => {
    // Create unique filename: timestamp-originalname
    const uniqueName = `${Date.now()}-${file.originalname}`;
    cb(null, uniqueName);
  },
});

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

Now files are saved as 1715350000000-photo.jpg instead of a1b2c3d4e5f6.

Adding File Validation

const upload = multer({
  storage,
  limits: {
    fileSize: 5 * 1024 * 1024, // 5 MB max
  },
  fileFilter: (req, file, cb) => {
    // Only allow images
    const allowedTypes = ["image/jpeg", "image/png", "image/gif", "image/webp"];

    if (allowedTypes.includes(file.mimetype)) {
      cb(null, true); // Accept
    } else {
      cb(new Error("Only image files are allowed!"), false); // Reject
    }
  },
});
Enter fullscreen mode Exit fullscreen mode

Handling Upload Errors

app.post("/upload/avatar", (req, res) => {
  upload.single("avatar")(req, res, (err) => {
    if (err instanceof multer.MulterError) {
      // Multer-specific error (file too large, too many files, etc.)
      return res.status(400).json({ error: err.message });
    }
    if (err) {
      // Custom error (wrong file type)
      return res.status(400).json({ error: err.message });
    }
    if (!req.file) {
      return res.status(400).json({ error: "No file uploaded" });
    }

    res.json({ message: "Upload successful!", file: req.file.originalname });
  });
});
Enter fullscreen mode Exit fullscreen mode

Serving Uploaded Files

After uploading, you need to make the files accessible. Use Express's static file middleware:

// Serve the uploads folder as static files
app.use("/uploads", express.static("uploads"));
Enter fullscreen mode Exit fullscreen mode

Now files are accessible via URL:

File saved at: uploads/1715350000000-photo.jpg
Accessible at: http://localhost:3000/uploads/1715350000000-photo.jpg
Enter fullscreen mode Exit fullscreen mode

Complete Upload + Serve Example

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

const app = express();
app.use(express.json());

// Storage config
const storage = multer.diskStorage({
  destination: "uploads/",
  filename: (req, file, cb) => {
    const ext = path.extname(file.originalname);
    cb(null, `${Date.now()}-${Math.round(Math.random() * 1000)}${ext}`);
  },
});

const upload = multer({
  storage,
  limits: { fileSize: 5 * 1024 * 1024 },
  fileFilter: (req, file, cb) => {
    const allowed = [".jpg", ".jpeg", ".png", ".gif", ".webp"];
    const ext = path.extname(file.originalname).toLowerCase();
    if (allowed.includes(ext)) cb(null, true);
    else cb(new Error(`File type ${ext} not allowed`));
  },
});

// Serve uploads as static files
app.use("/uploads", express.static("uploads"));

// Upload endpoint
app.post("/api/upload", upload.single("image"), (req, res) => {
  if (!req.file) return res.status(400).json({ error: "No file" });

  const fileUrl = `${req.protocol}://${req.get("host")}/uploads/${req.file.filename}`;

  res.status(201).json({
    message: "Uploaded!",
    file: {
      name: req.file.originalname,
      size: `${(req.file.size / 1024).toFixed(1)} KB`,
      url: fileUrl,
    },
  });
});

// Serve upload form
app.get("/", (req, res) => {
  res.send(`
    <h1>File Upload</h1>
    <form action="/api/upload" method="POST" enctype="multipart/form-data">
      <input type="file" name="image" accept="image/*" />
      <button type="submit">Upload</button>
    </form>
  `);
});

app.listen(3000, () => console.log("Server on http://localhost:3000"));
Enter fullscreen mode Exit fullscreen mode

Visit http://localhost:3000, upload an image, and the response will include a URL where you can view it.


Multer Middleware Execution Flow

POST /api/upload  (with file attached)
        │
        ↓
┌───────────────────────────────────────────────┐
│            MULTER MIDDLEWARE                    │
│                                               │
│  1. Check Content-Type                        │
│     → Is it multipart/form-data?              │
│     → NO  → skip (or error)                  │
│     → YES → continue                         │
│                                               │
│  2. Parse the multipart body                  │
│     → Separate text fields and file data      │
│                                               │
│  3. File filter check                         │
│     → Is this file type allowed?              │
│     → NO  → reject with error                │
│     → YES → continue                         │
│                                               │
│  4. Size limit check                          │
│     → Is file within size limit?              │
│     → NO  → reject with MulterError          │
│     → YES → continue                         │
│                                               │
│  5. Storage engine                            │
│     → Determine destination folder            │
│     → Generate filename                       │
│     → Write file to disk                     │
│                                               │
│  6. Populate request object                   │
│     → req.file = { originalname, path, ... }  │
│     → req.body = { text fields }              │
│                                               │
│  7. Call next()                               │
│     → Route handler runs                     │
│                                               │
└───────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Let's Practice: Hands-On Assignment

Part 1: Single File Upload

Build a server that:

  • Accepts a profile picture via POST /upload/avatar
  • Saves it to an uploads/ folder with a unique name
  • Returns the file URL in the response

Part 2: Multiple File Upload

Add a route POST /upload/gallery that:

  • Accepts up to 5 images
  • Returns info about all uploaded files

Part 3: Add Validation

Modify your upload to:

  • Only accept images (JPEG, PNG, GIF, WebP)
  • Limit file size to 2 MB
  • Return clear error messages when validation fails

Part 4: Serve and Display

  • Serve the uploads/ folder with express.static()
  • Create a GET /gallery route that returns HTML showing all uploaded images

Key Takeaways

  1. Files need special handling because they're sent as multipart/form-data — a format that express.json() can't parse. Multer is the middleware that bridges this gap.
  2. upload.single("field") handles one file (req.file). upload.array("field", max) handles multiple files from the same field (req.files). upload.fields() handles files from different fields.
  3. Storage configuration controls where and how files are saved. Use multer.diskStorage() for custom filenames and destinations.
  4. Always validate uploads — check file type with fileFilter, limit file size with limits, and handle errors gracefully.
  5. Serve uploads with express.static("uploads") to make files accessible via URL.

Wrapping Up

File uploads are one of those features that seem intimidating until you set up Multer — then it's just another middleware in your Express pipeline. Parse the multipart data, validate the file, save it, serve it. The same pattern whether you're handling profile pictures, document uploads, or image galleries.

I'm learning all of this through the ChaiCode Web Dev Cohort 2026 under Hitesh Chaudhary and Piyush Garg. File uploads were the feature that made my projects feel real — the moment users can upload their own images, the app stops being a demo and starts being a product.

Connect with me on LinkedIn or visit PrathamDEV.in. More articles on the way.

Happy coding! 🚀


Written by Pratham Bhardwaj | Web Dev Cohort 2026, ChaiCode

Top comments (0)