File Uploads with Multer

Reviewed & published by Brayan K

Multer is Express middleware for multipart/form-data that parses incoming file uploads, saving each file to disk or memory and exposing it on req.file or req.files so your route can store and validate it.

Part of the free Node.js course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

In this lesson you'll learn why uploads need a special parser, how to configure disk vs memory storage, how single, array, and fields differ, and how to lock things down with a fileFilter and size limits — the standard way Express apps accept avatars, photos, and documents.

What You'll Learn in This Lesson

1️⃣ Wiring Up Multer

A browser sends an uploaded file as multipart/form-data, not JSON, so express.json() can't touch it. Multer is the middleware that understands that format. You configure a storage engine — here diskStorage with a destination folder and a filename function — and then add upload.single('avatar') to the route.

After Multer runs, the file is on req.file (with filename, originalname, size, mimetype) and any plain text fields are on req.body. The filename function is where you control how files are named on disk — crucial for avoiding collisions.

// Install once:  npm install express multer
const express = require('express');
const multer = require('multer');

const app = express();

// diskStorage writes uploaded files straight to a folder on disk.
const storage = multer.diskStorage({
  destination: 'uploads/',                       // where files land
  filename: (req, file, cb) => {                 // what to name them
    const unique = Date.now() + '-' + Math.round(Math.random() * 1e9);
    cb(null, file.fieldname + '-' + unique);
  },
});
const upload = multer({ storage });

// upload.single('avatar') parses ONE file from the form field named "avatar".
// After it runs, the file is on req.file and any text fields are on req.body.
app.post('/profile', upload.single('avatar'), (req, res) => {
  res.json({
    saved: req.file.filename,        // the name on disk
    original: req.file.originalname, // what the user called it
    size: req.file.size,             // bytes
    name: req.body.name,             // a normal text field from the form
  });
});

app.listen(3000);

2️⃣ Naming, Filtering & Limits — the Logic

The trickiest parts of uploads aren't the server plumbing — they're the decisions: what to name a file so two uploads don't collide, which types to accept, and how big is too big. The block below runs as plain Node and demonstrates exactly the logic Multer applies for you: a unique filename, a mime-type filter, and a size cap.

const path = require('node:path');

// This is the same filename logic multer's diskStorage.filename runs for you.
function makeFilename(originalname) {
  const unique = '1718700000000-517274980';   // Date.now() + random (fixed here)
  const ext = path.extname(originalname);      // ".png", ".pdf", ...
  const base = path.basename(originalname, ext).replace(/\s+/g, '_');
  return base + '-' + unique + ext;
}
console.log(makeFilename('My Vacation.png'));
console.log(makeFilename('report final.pdf'));

// This mirrors multer's fileFilter: accept only certain mime types.
function fileFilter(mimetype) {
  const allowed = ['image/png', 'image/jpeg'];
  return allowed.includes(mimetype);
}
console.log('Accept image/png? ', fileFilter('image/png'));
console.log('Accept application/zip?', fileFilter('application/zip'));

// And this mirrors multer's limits.fileSize check (2 MB cap here).
const MAX = 2 * 1024 * 1024;
function withinLimit(bytes) { return bytes <= MAX; }
console.log('1.5 MB ok?', withinLimit(1.5 * 1024 * 1024));
console.log('5 MB ok? ', withinLimit(5 * 1024 * 1024));

3️⃣ Disk vs Memory, and Many Files

Multer ships two storage engines. diskStorage streams the file to a folder (best for keeping files on the server). memoryStorage holds it in RAM as a Buffer on req.file.buffer (best when you forward it straight to cloud storage). The example below uses memory storage and shows the real metadata Multer attaches after a small upload:

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

const app = express();
// memoryStorage keeps the file in RAM as a Buffer (req.file.buffer) —
// great when you forward it to S3 or process it without touching disk.
const upload = multer({ storage: multer.memoryStorage() });

app.post('/upload', upload.single('avatar'), (req, res) => {
  res.json({
    field: req.file.fieldname,       // "avatar"
    name: req.file.originalname,     // "hi.txt"
    mime: req.file.mimetype,         // "text/plain"
    bytes: req.file.size,            // size in bytes
  });
});

// (A test client POSTs a small multipart body to /upload — see the output.)
app.listen(3000);

For more than one file, swap the helper: upload.array('photos', 5) for many files under one field, or upload.fields([...]) for several named fields. And the two safety knobs — fileFilter and limits — are configured right on the Multer instance:

const multer = require('multer');

const upload = multer({
  storage: multer.diskStorage({ destination: 'uploads/' }),

  // Reject anything that isn't an image. Call cb(null, false) to skip the
  // file quietly, or pass an Error to fail the whole request.
  fileFilter: (req, file, cb) => {
    if (file.mimetype.startsWith('image/')) cb(null, true);
    else cb(new Error('Only image uploads are allowed'), false);
  },

  // Hard limits multer enforces for you.
  limits: {
    fileSize: 2 * 1024 * 1024,   // 2 MB per file
    files: 5,                    // at most 5 files
  },
});

// single  -> req.file        (one file)
// array   -> req.files        (many files, same field name)
// fields  -> req.files map    (different field names)
const handlers = {
  one: upload.single('avatar'),
  many: upload.array('photos', 5),
  mixed: upload.fields([{ name: 'avatar', maxCount: 1 }, { name: 'docs', maxCount: 3 }]),
};
module.exports = handlers;

Your turn. Fill in the two ___ blanks so the function builds a clean stored name, then run it.

const path = require('node:path');

// 🎯 YOUR TURN — replace each ___ then run it.
// Build a safe stored filename: <basename>-<id><ext>, e.g. "logo-7.png".

function storedName(originalname, id) {
  const ext = path.extname(originalname);                 // ".png"
  const base = path.basename(originalname, ___);          // 👉 ext  (strips ".png")
  return base + '-' + id + ___;                            // 👉 ext  (re-adds ".png")
}

console.log(storedName('logo.png', 7));
console.log(storedName('resume.pdf', 12));

// ✅ Expected output:
//    logo-7.png
//    resume-12.pdf

Mini-Challenge: An Upload Gatekeeper

No blanks this time — just a brief and an outline. Write the function yourself, run it, and check your output against the example. This is the same decision Multer's fileFilter and limits make.

// 🎯 MINI-CHALLENGE: a tiny upload gatekeeper (no server needed).
// Write checkUpload(file) where file = { mimetype, size }.
// Rules:
//   - allow only 'image/png' and 'image/jpeg'
//   - max size is 1 MB (1024 * 1024 bytes)
//   - return 'accepted', 'bad type', or 'too big'
// Then test it with the three calls below.

// your code here

// console.log(checkUpload({ mimetype: 'image/png', size: 500000 }));
// console.log(checkUpload({ mimetype: 'application/zip', size: 1000 }));
// console.log(checkUpload({ mimetype: 'image/jpeg', size: 5000000 }));
//
// ✅ Example output:
//    accepted
//    bad type
//    too big

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Multer

TaskSyntax
One fileupload.single('avatar')
Many filesupload.array('photos', 5)
Named fieldsupload.fields([{ name, maxCount }])
Save to diskmulter.diskStorage({ destination, filename })
Keep in RAMmulter.memoryStorage()
Cap size{ limits: { fileSize } }
Filter types{ fileFilter: (req, file, cb) => ... }

🎉 Lesson Complete!

Practice quiz

Why can't express.json() parse a file upload?

  • Files are too large for it
  • Uploads are sent as multipart/form-data, a different encoding it doesn't understand
  • It only runs on GET requests
  • It needs a database first

Answer: Uploads are sent as multipart/form-data, a different encoding it doesn't understand. express.json() only parses application/json; uploads arrive as multipart/form-data, which needs Multer.

After upload.single('avatar') runs, where is the uploaded file?

  • req.body.avatar
  • req.params.file
  • req.file
  • req.upload

Answer: req.file. upload.single puts the single file on req.file and any text fields on req.body.

Which storage engine keeps the file in RAM as a Buffer on req.file.buffer?

  • multer.memoryStorage()
  • multer.diskStorage()
  • multer.bufferStorage()
  • multer.ramStorage()

Answer: multer.memoryStorage(). memoryStorage holds the file in RAM as a Buffer, ideal when forwarding it straight to cloud storage.

Which helper accepts up to five files all sharing one field name?

  • upload.single('photos')
  • upload.fields('photos', 5)
  • upload.many('photos')
  • upload.array('photos', 5)

Answer: upload.array('photos', 5). upload.array('photos', 5) accepts many files under one field name and puts them on req.files.

What does the limits.fileSize option do?

  • Sets the upload folder
  • Caps the bytes per file, aborting with a MulterError if exceeded
  • Renames the file
  • Counts the number of fields

Answer: Caps the bytes per file, aborting with a MulterError if exceeded. limits.fileSize caps each file's size; Multer aborts with a MulterError when a file is too large.

Inside a fileFilter, how do you reject a file and fail the whole request?

  • return false
  • cb(null, true)
  • cb(new Error('not allowed'), false)
  • throw 'no'

Answer: cb(new Error('not allowed'), false). Pass an Error to cb to fail the request; cb(null, false) skips the file quietly.

Why should you never write the user's original filename straight to disk?

  • It is always too long
  • Multer forbids it
  • It is slower than a random name
  • A user could supply a path like ../../etc/passwd, so generate your own name and borrow only the extension

Answer: A user could supply a path like ../../etc/passwd, so generate your own name and borrow only the extension. The original name is untrusted; generate a safe unique name and keep only the extension.

req.file is undefined after an upload. What is a likely cause?

  • The form field name doesn't match the name in upload.single(), or the form lacks multipart/form-data
  • The file was too small
  • memoryStorage was used
  • The server is on the wrong port

Answer: The form field name doesn't match the name in upload.single(), or the form lacks multipart/form-data. The form field name must match upload.single's name and the form must use enctype multipart/form-data.

Why is trusting file.mimetype alone not enough for security?

  • It is always empty
  • It is supplied by the client and can be spoofed
  • It is only set for images
  • Multer deletes it

Answer: It is supplied by the client and can be spoofed. mimetype comes from the client and can be faked, so also validate real contents server-side.

Which upload helper accepts files from several differently-named fields at once?

  • upload.fields([{ name: 'avatar', maxCount: 1 }, { name: 'docs', maxCount: 3 }])
  • upload.single('all')
  • upload.array('fields')
  • upload.any('avatar', 'docs')

Answer: upload.fields([{ name: 'avatar', maxCount: 1 }, { name: 'docs', maxCount: 3 }]). upload.fields([...]) accepts several named fields and keys them on req.files by field name.

Continue this course

Frequently asked questions

Why can't express.json() parse a file upload?

express.json() only understands an application/json body. File uploads from an HTML form are sent as multipart/form-data, a completely different encoding that interleaves file bytes and text fields with boundary markers. You need a parser that understands that format — that's exactly what Multer is. Add Multer's middleware (upload.single, upload.array, etc.) to the route that receives the upload, and it will populate req.file / req.files and put any text fields on req.body.

Should I use diskStorage or memoryStorage?

Use diskStorage when you want the file saved to a folder on the server and you're happy serving it from there or moving it later — it streams to disk so it handles large files without eating memory. Use memoryStorage when you immediately forward the file somewhere else (an S3 bucket, an image resizer, a virus scanner) and never want it on the local disk; the file arrives as a Buffer on req.file.buffer. Memory storage is convenient but holds the whole file in RAM, so always pair it with a sensible fileSize limit.

What's the difference between upload.single, upload.array, and upload.fields?

They differ by how many files and how many field names you accept. upload.single('avatar') accepts exactly one file from the field named 'avatar' and puts it on req.file. upload.array('photos', 5) accepts up to five files all sharing the field name 'photos' and puts them on req.files as an array. upload.fields([...]) accepts files from several differently-named fields at once (for example one 'avatar' plus up to three 'docs') and puts them on req.files keyed by field name. Pick the one that matches your form's input fields.

How do I stop people from uploading huge or dangerous files?

Combine two of Multer's options. limits: { fileSize, files } caps the bytes per file and the number of files, and Multer aborts with a MulterError if a request exceeds them. fileFilter lets you inspect file.mimetype (and originalname) and call cb(null, false) to skip a file or cb(new Error(...)) to reject the request — for example allowing only image/* types. Note that mimetype is supplied by the client and can be spoofed, so for true safety also validate the file's real contents server-side and store uploads outside any folder that gets executed.