Skip to main content

Command Palette

Search for a command to run...

File uploads in Express: a Multer guide

Updated
4 min read
File uploads in Express: a Multer guide
S
A front-end developer who’s always learning, building projects, and writing blogs to simplify web concepts

Most HTTP requests carry simple text. A JSON body, some query params. The server reads it, done.

File uploads are different. When a browser sends a file, it wraps everything in multipart/form-data, a special encoding that can carry binary data alongside text fields. Raw Node.js doesn't know how to read that. It sees a stream of bytes and stops there.

That's why you need middleware.


What Multer is -

Multer is an Express middleware that reads multipart/form-data requests and makes sense of them. It parses the incoming stream, pulls out the file(s), writes them somewhere, and then attaches metadata to req.file (or req.files for multiple uploads) so your route handler can use it.

One line of setup, then it's invisible.

npm install multer

Single file upload

Say you have a profile picture upload. One file, one field.

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

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

app.post('/profile', upload.single('avatar'), (req, res) => {
  console.log(req.file); // the uploaded file
  res.send('Upload successful');
});

upload.single('avatar') tells Multer to look for a field named avatar in the form. After it runs, req.file has everything you need: original name, mime type, file size, and the path where it was saved.

The dest: 'uploads/' option is the simplest storage setting. Multer handles the rest.


Multiple file uploads

Two patterns here, depending on what you're expecting.

Multiple files under the same field (like a gallery upload):

app.post('/gallery', upload.array('photos', 10), (req, res) => {
  console.log(req.files); // array of file objects
  res.send(`Uploaded ${req.files.length} files`);
});

The 10 is a limit. Multer rejects requests with more files than that.

Multiple files across different fields (like a form with a resume and a cover letter):

const cpUpload = upload.fields([
  { name: 'resume', maxCount: 1 },
  { name: 'cover_letter', maxCount: 1 }
]);

app.post('/apply', cpUpload, (req, res) => {
  console.log(req.files['resume'][0]);
  console.log(req.files['cover_letter'][0]);
  res.send('Application received');
});

Here req.files is an object keyed by field name.


Storage configuration

The dest shortcut is fine for prototyping, but Multer gives you real control through diskStorage.

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

const upload = multer({ storage });

Two callbacks. destination controls the folder. filename controls the name on disk.

The Date.now() trick avoids collisions. Two users uploading photo.jpg at the same time get different filenames.

You can also add file validation here. Reject anything that isn't an image:

const upload = multer({
  storage,
  fileFilter: (req, file, cb) => {
    if (!file.mimetype.startsWith('image/')) {
      return cb(new Error('Only images allowed'));
    }
    cb(null, true);
  },
  limits: { fileSize: 5 * 1024 * 1024 } // 5 MB cap
});

Serving uploaded files

Once files are on disk, you need a way for clients to fetch them back. Express makes this easy.

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

Now GET /uploads/1714935182034-823740918.jpg serves that file directly. Done.

A couple things to know about this: the path in the URL matches the folder path relative to where you're serving from. And this setup serves everything in that folder publicly. For user-uploaded content you probably want access control eventually, but for getting started, static serving is enough.

---The middleware runs in that exact order every time. If fileFilter rejects the file, execution stops there and your route handler never sees it.


conclusion

A file gets wrapped, sent over HTTP, and unpacked on the server. Multer just sits in the middle and handles that handoff. Once you see it once, it stops feeling complex.

Start small. Use dest: 'uploads/' and upload.single(). Make sure the full flow works. Then add control where you need it, custom filenames, type checks, size limits. Each step is isolated.

Most people overcomplicate this early. They jump to S3 or Cloudinary before understanding what’s happening locally. That slows everything down. Get it working on disk first. Inspect req.file. Break it. Fix it. Then move storage if needed.

Multer has stayed relevant for a reason. It’s focused, predictable, and does exactly what you need without getting in the way.

File Uploads in Express: A Multer Guide