# Handling File Uploads in Express with Multer

So at some point in almost every project, you need allow users to upload something it can be a profile picture, document, CSV and the first time you can try to handle that with just ***express.json()*** you can quickly realize that it doesn't work like that. Nothing shows up in ***req.body, req.file*** is undefined, and you're confused about it why.

The reason is that file uploads can work differently from regular JSON requests. Let me explain this why, and then show you how Multer fixes it.

## Why File Uploads Need Special Handling

When you send JSON from a form or API call, the data goes as plain text in the request body. Express's built-in ***express.json()*** middleware knows how to read that.

But files are different. When a form submits with a file attached, the browser uses a special encoding called **multipart/form-data**. Instead of one blob of JSON, the request gets split into multiple parts one part for each field, one part for each file. Each part has its own headers, its own content, its own boundaries separating them.

***express.json()*** has no idea what to do with that. It's built for JSON, not multipart data. So you need something specifically designed to parse multipart requests and pull out the files and that's exactly what Multer does.

## What is Multer?

Multer is an Express middleware for handling ***multipart/form-data***. Its whole job is to intercept file uploads, parse the multipart request, and give you access to the uploaded files through ***req.file*** (single upload) or ***req.files*** (multiple uploads).

It also handles where the file goes either saved to disk or kept in memory and gives you control over file naming, size limits, and file type filtering.

Install it first:

```shell
npm install multer
```

Basic setup:

```javascript
const multer = require("multer")
const upload = multer({ dest: "uploads/" })
```

That `dest` option tells Multer where to save uploaded files. For now this is the simplest way to get going

## The Upload Lifecycle

Before jumping into code, here's what actually happens when a file upload request hits your server:

![](https://cdn.hashnode.com/uploads/covers/695bac23a1a77cade35e2528/070ee4f1-68e0-4e9f-8fa4-8b0d6c384acc.png align="center")

By the time your route handler runs, Multer has already dealt with the file. You just use what it gives you.

## Handling a Single File Upload

```javascript
const express = require("express")
const multer = require("multer")

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

app.post("/upload", upload.single("avatar"), (req, res) => {
  console.log(req.file)
  res.json({ message: "File uploaded", file: req.file })
})

app.listen(3000)
```

upload.single("avatar") is the middleware. The string `"avatar"` is the field name it has to match the `name` attribute of the file input in your form, or the field name in Postman.

Once the upload is done, ***req.file*** has all the info you need:

```javascript
{
  fieldname: 'avatar',
  originalname: 'profile.jpg',
  encoding: '7bit',
  mimetype: 'image/jpeg',
  destination: 'uploads/',
  filename: 'a3f9c2d1b4e7...', 
  path: 'uploads/a3f9c2d1b4e7...',
  size: 204800
}
```

Notice ***filename*** Multer generates a random name by default and strips the extension. That's why storage configuration matters if you want readable filenames.

## Handling Multiple File Uploads

Two ways to do this depending on what you need.

**Multiple files from the same field:**

```js
app.post("/upload-many", upload.array("photos", 5), (req, res) => {
  console.log(req.files)
  res.json({ count: req.files.length, files: req.files })
})
```

***upload.array("photos", 5)*** first argument is the field name, second is the max number of files allowed. Files come through as **req.files** an array.

**Multiple files from different fields:**

```javascript
app.post("/upload-mixed", upload.fields([
  { name: "avatar", maxCount: 1 },
  { name: "resume", maxCount: 1 }
]), (req, res) => {
  console.log(req.files.avatar)
  console.log(req.files.resume)
  res.json({ message: "Both files received" })
})
```

upload.fields() takes an array of field configs. `req.files` is now an object where each key is a field name and the value is an array of file objects for that field.

## Storage Configuration

The **dest** shortcut works fine for quick testing, but in real projects you want control over filenames and where exactly things go. Multer's `diskStorage` engine gives you that.

```javascript
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, "uploads/")
  },
  filename: (req, file, cb) => {
    const uniqueName = Date.now() + "-" + file.originalname;
    cb(null, uniqueName)
  }
})

const upload = multer({ storage })
```

**destination** controls which folder the file goes into. **filename** controls what the file is named on disk. That **cb** is a callback first argument is an error, second is the value you're setting.

Now instead of a random hash, your uploaded file gets a name like 1714823400000-profile.jpg. Much more manageable.

You can also add file size limits:

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

And filter by file type:

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

cb(null, true) accepts the file. cb(null, false) rejects it silently. cb(new Error(...), false) rejects with an error you can catch.

## Serving Uploaded Files

Once files are saved to disk, you need a way to actually serve them back. express.static() handles this.

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

Now any file inside your uploads/ folder is accessible at http://localhost:3000/uploads/filename.jpg.

So after uploading, you can send back the file URL:

```javascript
app.post("/upload", upload.single("avatar"), (req, res) => {
  const fileUrl = `http://localhost:3000/uploads/${req.file.filename}`
  res.json({ url: fileUrl })
})
```

Client stores that URL, uses it wherever profile picture, document preview, whatever the use case is.

## Handling Upload Errors

Multer throws errors for things like file size exceeded or wrong file type. These need to be caught specifically because they're Multer errors, not generic Express errors.

```javascript
app.post("/upload", (req, res) => {
  upload.single("avatar")(req, res, (err) => {
    if (err instanceof multer.MulterError) {
      return res.status(400).json({ message: err.message })
    } else if (err) {
      return res.status(400).json({ message: err.message })
    }
    res.json({ file: req.file })
  })
})
```

This inline style gives you direct access to the error before it hits your general error handler. multer.MulterError covers things like file too large, too many files, unexpected field. The generic err catch covers your custom fileFilter errors.

## Full Example Together

```js
const express = require("express")
const multer = require("multer")
const path = require("path")

const app = express()

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,
  limits: { fileSize: 2 * 1024 * 1024 },
  fileFilter: (req, file, cb) => {
    file.mimetype.startsWith("image/")
      ? cb(null, true)
      : cb(new Error("Images only"), false)
  }
})

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

app.post("/upload", upload.single("avatar"), (req, res) => {
  const url = `http://localhost:3000/uploads/${req.file.filename}`
  res.json({ message: "Uploaded", url })
})

app.listen(3000, () => console.log("Running on 3000"))
```

Storage config, size limit, type filter, static serving all in one place. This is the pattern you'd start a real upload feature with.

The first time file uploads work end-to-end in your own project is genuinely satisfying. Request comes in, file lands on disk, URL goes back to the client. Once you have this working locally, the next step would be replacing local disk storage with something like Cloudinary or S3 but that's a separate topic. Get comfortable with this pattern first.
