# Handling File Uploads in Express with Multer

## 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.

![](https://cdn.hashnode.com/uploads/covers/69413d2ffd5a397514bc42f5/d8c0c569-cbc4-4baf-8f48-f0359436334d.png align="center")

* * *

### 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.

![](https://cdn.hashnode.com/uploads/covers/69413d2ffd5a397514bc42f5/4b1bbe88-3441-410d-a470-7999949b9cad.png align="center")

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:

```shell
npm install express multer
```

Here is the full journey a file takes.

![](https://cdn.hashnode.com/uploads/covers/69413d2ffd5a397514bc42f5/3c84c4ed-8030-4016-8637-afa4f8d82ba0.png align="center")

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.

```html
<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:

```javascript
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.

![](https://cdn.hashnode.com/uploads/covers/69413d2ffd5a397514bc42f5/c96f2dee-35c1-4420-ac3b-afc7a1d09756.png align="center")

You can test it without a browser on postman application:

![](https://cdn.hashnode.com/uploads/covers/69413d2ffd5a397514bc42f5/e829c161-a136-47f7-be76-0029d9afdfda.png align="center")

### 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.

```javascript
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**.

```javascript
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.

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

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

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

Return that URL from your upload route:

```javascript
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.
    

![](https://cdn.hashnode.com/uploads/covers/69413d2ffd5a397514bc42f5/eef49241-8987-4300-a3b1-b1e5cff7bec8.png align="center")

Here is the final server in one block:

```javascript
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.

<details><summary><strong>Solution: </strong></summary>
<pre><code class="language-javascript">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);
    }
  },
});
</code></pre>
</details>

* * *

## 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.
