Skip to main content

Command Palette

Search for a command to run...

Handling File Uploads in Express with Multer

Updated
•10 min read•View as Markdown
Handling File Uploads in Express with Multer

Hello reader, in earlier blogs for express, you have learn how to send data through payload and through url, i.e. using url parameters and query parameters, this blog is dedicated to uploading file in express. on paper it might look similar to JSON but it varies from it. By the end of blog, you would be confident to design routes to upload your own files in express.


Why file uploads need middleware

Handling normal data (using JSON) is quite simple for express, you simple do:

app.use(express.json())

and this shall parse all the incoming request body for you in proper way.

However it is different in case of a file upload. Files do not come as mere JSON, but they come as multipart/form-data. This format:

  • Splits data into multiple parts

  • Sends file as binary chunks

  • Mixes text fields + files together

Now this is something which cannot be parse by default, and till the time of writing this blog, express does not have it's own middleware which you would simply use and get started with file uploads. So without any middleware req.body is incomplete and file data is not accessible. This is why we need a specialised middleware which can handle multipart/form-data and make development process related to files easier.

What is Multer ?

In the above section you understood that express is incapable of parsing files, and we needed a specialised middleware to do our bidding.

Multer is the protagonist of this blog. It is a middleware built specifically to handle file uploads, multer is built on top of busybox. What is busybox is the scope of some another blog, we are here to understand how to work around with multer and get file uploads integrated in out backend systems.

Multer does all the heavy lifting for the file upload operations:

  • Reads incoming stream

  • Parses multipart data

  • Extracts files

  • Stores them (disk or memory)

It makes them available in:

req.file   // single file
req.files  // multiple files

Upload lifecycle

Now that you have understood that why we are using multer for, it is important to understand where it sits in the upload lifecycle and what exactly happens at each step.

Client uploads file
        ↓
Multer middleware intercepts request
        ↓
Parses multipart data
        ↓
Stores file
        ↓
Attaches file info to req
        ↓
Route handler runs

1. Client uploads file

The process starts from the client (browser or app ) usually through a form.

<form action="/upload" method="POST" enctype="multipart/form-data">
  <input type="file" name="photo-file" />
  <button type="submit">Upload</button>
</form>

Notice the important part:

enctype="multipart/form-data"

This tells the browser:

“I’m sending files, not JSON.”

Instead of a clean JSON object, the request becomes a stream of mixed data:

  • file chunks

  • text fields

  • metadata

The second thing worth noticing is:

name="photo-file" 

Here photo-file is the field name. When the browser sends the request, it packages the file like:

photo-file: <actual file data>

So photo-file becomes the key for that file in the request.

2. Multer middleware intercepts the request

When the request reaches your express server, it doesn’t go directly to your route handler. Multer sits in between, just like any middleware:

app.post("/upload", upload.single("file"), handler);

Here, upload.single("photo-file") runs before your handler. Notice in the previous step we discuss about photo-file. This is where it connects with the multer, and you must use the same field name.

If you don't match the file name that you used in the client side then:

  • Multer won’t find the file

  • req.file will be undefined

3. Parses multipart data

Now multer takes control of the incoming stream. You are spared from coding from scratch handlers to listen the stream i.e. you no longer need to use req.on("data") , buffering chunks or parsing boundaries. Multer shall read the stream and separates file data from text fields and understands multipart boundaries. Multer does all of this behind the hood.

4. Stores the file

Once the file is extracted, Multer stores it based on your configuration:

  • Disk storage → saves file to a folder

  • Memory storage → keeps file in RAM

Example:

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

File is physically saved before your route runs.

5. Attaches file info to req

Saving the files isn't the last step in multer, because multer goes to next level and enriches the request object with req.file. The req.file contains:

  • filename

  • originalname

  • path

  • size

  • etc.

If there were multiple files then in that case instead of req.file you get req.files.

You see your route never deals with raw file streams, multer processes everything before your logic even starts. Understanding the lifecycle shall help you:

  • Debug issues like missing files

  • Know where validation should happen

  • Understand why middleware is required

  • Avoid trying to access req.file before Multer runs

Setting up the multer middleware.

In most blogs that you read on this topic will provide the below code to set up the middleware:

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

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

well the above code will work but this isn't project ready, I won't even use this in a fun project, because it's like having the infinity stones but not using them at all. Let us set up our middleware for multer:

npm install multer

The very first step is to install multer, then let us see the rest of process in parts:

1. Storage

First you need to understand where you want to store the file that is uploaded, you have two options with you:

  • Disk Storage

  • Memory Storage

The memory storage engine stores the files in memory as Buffer objects. It doesn't have any options.

const storage = multer.memoryStorage()
const upload = multer({ storage: storage })

We would be using Disk storage in this blog:

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, UPLOAD_DIR);
  },
  filename: (req, file, cb) => {
    const ext = path.extname(file.originalname);
    cb(null, `\({Date.now()}-\){crypto.randomBytes(4).toString("hex")}${ext}`);
  },
});

There are two options available, destination and filename. They are both functions that determine where the file should be stored.

destination is used to determine within which folder the uploaded files should be stored.

Both of them receive a function, the first parameter is the express request, the second parameter is the file to be uploaded and third is a callback. The callback will take the first parameter as error.

You would want your files to have unique names and it's upon you how you would manage them.

2. Limits

You might want to restrict the user from loading too big of a file, because it could cause strain on your memory.

const limits = { fileSize: 5 * 1024 * 1024 };

The file limit is in bytes, so if you want to allow only 5mb then it's 5*1024*1024. Limit does not deal only with file size but there are other things you might want to limit as in below diagram:

3. File filter

You might want to check if the file uploaded by the user is the file that you accept or not, if not then you should throw an error in that case.

const fileFilter = (req, file, cb) => {
  if (["image/jpeg", "image/png", "image/gif"].includes(file.mimetype)) {
    return cb(null, true);
  }
  return cb(
    new Error("Invalid file type. Only JPEG, PNG, and GIF are allowed."),
    false,
  );
};

In the above function I have decided only to accept images and nothing else.

Now that you have the major setting, it's time you combine them:

export const upload = multer({ storage, fileFilter, limits });

you are exporting your multer as upload middleare. In the next section let us learn how to use this upload middleware.

Handling file uploads

1. Single file upload

To handle single file uploads you simply do:

app.post("/upload", upload.single("file"), (req, res) => {
  console.log(req.file);
  res.send("File uploaded");
});

2. Multiple file upload

app.post("/uploads", upload.array("files", 3), (req, res) => {
  console.log(req.files);
  res.send("Multiple files uploaded");
});

Serving uploaded files

You can access the files that you stored, you simply need to serve them

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

Now this means that the same file shall be available via:

http://localhost:3000/uploads/filename.jpg

If you want the file to ben sent for the user to download you can do:

app.get("/download/:filename", (req, res) => {
  const filePath = `uploads/${req.params.filename}`;

  res.set({
    "Content-Disposition": `attachment; filename="${req.params.filename}"`
  });

  res.sendFile(filePath, { root: __dirname });
});

What these headers do Content-Disposition

attachment; filename="image.jpg"

This tells the browser:

“Don’t display this — download it as a file”

Without this header

  • Image → opens in browser

  • PDF → opens in viewer

With this header

  • Forces download dialog

Even cleaner way is to take from express because express already provides a helper:

res.download(filePath);

This internally:

  • Sets Content-Disposition

  • Sends file

Security (BONUS)

Temporary Files

When using Multer with disk storage, files are written to disk immediately. This means that even invalid or malicious files are temporarily saved on your server.

Because of this, it is important to treat uploaded files as temporary until they are validated. If a file fails validation, it should be immediately removed from the filesystem.

Failing to do so can lead to unnecessary storage usage and potential security risks, especially if unverified files are left accessible on the server.

A common practice is to delete invalid files right after validation or use a temporary folder for uploads and move only valid files to a permanent location.

Securing the flow

You have already set up a basic level of file validation through multer, but there are hackers, who can bye pass the system like:

  • Fake file extension: Rename a malicious file:

    virus.exe → image.jpg
    

    Your system sees .jpg and allows it.

  • Fake MIME type: Multer relies on file.mimetype, which comes from the client.
    An attacker can send:

    Content-Type: image/jpeg
    

    Even if the file is not an image.

  • Changing file Content: A file can:

    • Look like an image externally

    • But contain malicious payload inside

    This is especially dangerous when:

    • Files are executed

    • Or processed by other systems

import { fileTypeFromFile } from "file-type";
import fs from "fs/promises";


export const uploadImage = async (req, res, next) => {
  try {
    if (!req.file) {
      return res.status(400).json({
        error: { message: "No file uploaded" },
      });
    }

    const filePath = req.file.path;

    const type = await fileTypeFromFile(filePath);

    const allowedMimeTypes = [
      "image/jpeg",
      "image/png",
      "image/gif",
      "application/pdf",
    ];

    if (!type || !allowedMimeTypes.includes(type.mime)) {
      await fs.unlink(filePath);

      return res.status(400).json({
        error: { message: "Invalid file content" },
      });
    }

    res.status(200).json({ message: "File uploaded successfully" });

  } catch (err) {
    next(err);
  }
};

The above code block uses an npm package file-type which shall help us in determining if the file is what it says it is.

Conclusion

Uploading files seems like a difficult task if you are not aware with the basics, but once you are familiar with the lifecycle and libraries like multer, it is a piece of cake.

Happy Coding !