Skip to main content

Command Palette

Search for a command to run...

Handling File Uploads in Express with Multer

Updated
•7 min read•View as Markdown
Handling File Uploads in Express with Multer
S
I'm a passionate software engineer and full-stack MERN developer who loves to turn ideas into scalable, user-centric applications. I have hands-on experience in building modern web solutions using React, Node.js, Express.js, MongoDB, following clean architecture and best development practices. My experience in the Cognizant Healthcare Product Consulting (HPC) program has given me hands-on exposure to SQL, PL/SQL, U.S. healthcare payer systems and TriZetto Facets, and has helped me to further develop my skills in working with enterprise software in domain-driven environments. I enjoy tackling complex technical problems, constantly learning, and building reliable applications that deliver business value.

Why can't Express read a file upload on its own?

You build a form with a file input. You submit it. You log req.body.

It is empty. Many beginners think Express is broken or that it express.json() should handle it. That's not true.

A file upload is not JSON. It travels in a different format called multipart/form-data.

multipart/form-data is a request format that splits one request into several parts, so text fields and raw files can travel together.


Analogy: The Courier Counter

Think of a courier office.

  • Your request = a parcel box with compartments

  • Text fields = the letter inside the box

  • The file = the package inside the box

  • Express = the office building

  • Multer = the clerk at the counter

The building alone cannot open the box. Someone has to sit at the counter, open it, and separate the letter from the package.

So why does Express need a clerk at all? Let's look at what goes wrong without one.


What is Multer?

Express body parsers read JSON and URL-encoded data. They skip multipart data completely.

Multer is a middleware that reads multipart requests and does two jobs:

  1. It saves the file somewhere.

  2. It fills req.body with the text fields and req.file with the file details.

Multer is the clerk that opens the parcel before it reaches your route handler.

Install it:

npm install express multer

Here is the full journey a file takes.

  1. The browser sends a multipart request.

  2. Express receives it and passes it to Multer.

  3. Multer writes the file to storage.

  4. Your route handler runs with req.file ready.

Now that the clerk is hired, let's see the simplest job: one file.


How does a single file upload work?

First, the form. The enctype attribute is what tells the browser to use multipart.

<form action="http://localhost:8080/upload" method="POST" enctype="multipart/form-data">
  <input type="file" name="avatar" />
  <button type="submit">Upload</button>
</form>

Now the server:

import express from "express";
import multer from "multer";

const app = express();

const upload = multer({ dest: "uploads/" });

app.post("/upload", upload.single("avatar"), (req, res) => {
  if (!req.file) {
    return res.status(400).json({ 
        error: {
            message: "No file received",
        },
    });
  }

  res.json({ message: "Uploaded", filename: req.file.filename });
});

app.listen(8080, () => console.log("Server running on port 8080"));

Use "type": "module" in package.json for the import syntax.

The middleware sits before the handler. That order is the whole trick.

You can test it without a browser on postman application:

Note: Field names must match

The string in upload.single("avatar") must match the name in the HTML input.

If they differ, Multer throws an Unexpected field error. This is the most common beginner mistake.

One file works. But what if a user wants to upload five photos?


How do we accept multiple files?

You only change the middleware. The clerk now accepts several packages.

app.post("/gallery", upload.array("photos", 5), (req, res) => {
  const names = req.files.map((file) => file.filename);

  res.json({ count: req.files.length, files: names });
});

Notice the difference: req.files (plural) is an array.

Method Accepts Result on request
upload.single("avatar") One file, one field req.file
upload.array("photos", 5) Many files, one field (max 5) req.files (array)
upload.fields([...]) Many files, different fields req.files (object)

For the HTML form, add the multiple attribute to the input.

Right now the files land in a folder with random names. Let's take control of that.


Where do the files actually go?

The dest option is quick, but it gives files random names with no extension. For real projects, use diskStorage.

import path from "path";

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, "uploads/");
  },
  filename: (req, file, cb) => {
    const unique = Date.now() + "-" + Math.round(Math.random() * 1e9);

    cb(null, unique + path.extname(file.originalname));
  },
});

const upload = multer({
  storage,
  limits: { 
    fileSize: 2 * 1024 * 1024 // 2 MB 
  },
});

In our analogy:

  • destination = which shelf the package goes on

  • filename = the label stuck on the package

  • limits = the weighing scale that rejects heavy parcels

Create the uploads folder before running the server. Multer will not always create it for you.

Important Rule: Never trust the original filename.

Two users can upload photo.jpg. One overwrites the other.

That is why we build a unique name with a timestamp. We keep only the extension from the original.

We are skipping cloud storage on purpose. Local disk first, so the flow is clear before we add complexity.

The package is on the shelf. How does the customer collect it?


How do users see the uploaded files?

Files in a folder are not reachable from the browser. You must open a pickup window.

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

Now a file saved as uploads/1736000000-482.jpg is available at:

http://localhost:3000/uploads/1736000000-482.jpg

Return that URL from your upload route:

res.json({ url: `/uploads/${req.file.filename}` });

express.static is the pickup window. Multer receives, static serves.

Let's put every piece in one place.


How does the whole upload lifecycle fit together?

Here is the complete round trip, using the courier counter from start to finish.

  1. The browser packs a parcel (multipart/form-data).

  2. Express receives the parcel at the office.

  3. Multer opens it, checks the weight, labels it, and shelves it.

  4. Your route handler gets the receipt (req.file) and replies with a URL.

  5. Later, the browser asks for that URL.

  6. express.static fetches the package from the shelf and hands it over.

Here is the final server in one block:

import express from "express";
import multer from "multer";
import path from "path";

const app = express();

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, "uploads/");
  },
  filename: (req, file, cb) => {
    const unique = Date.now() + "-" + Math.round(Math.random() * 1e9);

    cb(null, unique + path.extname(file.originalname));
  },
});

const upload = multer({
  storage,
  limits: {
    fileSize: 2 * 1024 * 1024, // 2 MB
  },
});

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

app.post("/upload", upload.single("avatar"), (req, res) => {
  if (!req.file) {
    return res.status(400).json({
      error: {
        message: "No file received",
      },
    });
  }

  res.json({ url: `/uploads/${req.file.filename}` });
});

app.post("/gallery", upload.array("photos", 5), (req, res) => {
  res.json({ urls: req.files.map((f) => `/uploads/${f.filename}`) });
});

app.use((err, req, res, next) => {
  if (err instanceof multer.MulterError) {
    return res.status(400).json({
      error: {
        message: err.message,
      },
    });
  }

  next(err);
});

app.listen(8080, () => console.log("Server running on port 8080"));

The last block catches Multer errors, such as a file that is too large.


Try it yourself: reject non-image files

Add a fileFilter to the Multer config so only images are accepted.

Solution:
const upload = multer({
  storage,
  fileFilter: (req, file, cb) => {
    if (file.mimetype.startsWith("image/")) {
        cb(null, true);
    } else {
        cb(new Error("Only images are allowed"), false);
    }
  },
});

Conclusion

  • Middleware is needed because Express cannot read multipart data on its own.

  • Multer opens the multipart request and fills req.file or req.files.

  • Single upload uses upload.single(), multiple uses upload.array().

  • diskStorage controls the folder and the filename.

  • express.static lets the browser fetch what you saved.

If this felt like a lot of moving parts, that's okay. What matters is understanding the flow: parcel in, clerk opens it, shelf stores it, window serves it.

What's Next?

You can now accept files with Multer. But where should those files live for real, and how do you serve them safely?

In the next article, Storing Uploaded Files and Serving Them in Express, we will go deeper into storage choices and how to deliver files back to your users.


If you found this useful, drop a comment or a reaction.

More from this blog

Sahil Gupta | Web Development, Frontend, Backend & DevOps

51 posts

I'm a passionate software engineer and full-stack MERN developer who loves to turn ideas into scalable, user-centric applications. I have hands-on experience in building modern web solutions using React, Node.js, Express.js, MongoDB, following clean architecture and best development practices. My experience in the Cognizant Healthcare Product Consulting (HPC) program has given me hands-on exposure to SQL, PL/SQL, U.S. healthcare payer systems and TriZetto Facets, and has helped me to further dev