DEV Community

Harman Panwar
Harman Panwar

Posted on

Handling File Uploads in Express with Multer

File Uploads in Node.js: Understanding Multer and the Upload Lifecycle

Handling file uploads is a common requirement in web applications — profile pictures, document attachments, media uploads. Unlike regular form data, files can't be sent as simple JSON or URL-encoded strings. They require special handling through multipart form-data, and Node.js needs middleware to process this format. This guide explains why middleware is essential, how Multer solves this problem, and how to handle uploads from start to finish.


Why File Uploads Need Middleware

The Problem with Regular HTTP Requests

When you submit a standard HTML form, the browser sends data in one of two formats:

  1. application/x-www-form-urlencoded — Key-value pairs in the URL format (e.g., name=John&age=30)
  2. application/json — Structured data as a JSON string

Both formats work fine for text, numbers, and booleans. But files are different. A single image can be several megabytes of binary data — not something you can cleanly encode into a URL string or JSON text.

What is Multipart Form-Data?

Multipart form-data is a special content type (multipart/form-data) designed specifically for sending mixed content — text fields and binary files — in a single HTTP request.

Think of it like mailing a package with multiple items inside:

  • Each item (a text field or a file) is placed in its own compartment (a "part")
  • Each compartment has a label (the field name) and metadata (content type, filename)
  • The compartments are separated by a boundary — a unique string that marks where one part ends and another begins
  • The entire package is sent as the request body
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="username"

john_doe
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="avatar"; filename="profile.jpg"
Content-Type: image/jpeg

[Binary image data here...]
------WebKitFormBoundary7MA4YWxkTrZu0gW--
Enter fullscreen mode Exit fullscreen mode

Key insight: The server receives one long stream of bytes containing multiple "parts." It needs to:

  1. Identify each boundary to separate parts
  2. Parse headers for each part to know what it is
  3. Extract text values for regular fields
  4. Capture binary streams for files and save them to disk
  5. Handle all of this without loading everything into memory at once (files can be huge!)

This is far too complex to handle manually in every route. That's where middleware comes in.

Why Express Can't Handle It Alone

Express's built-in express.json() and express.urlencoded() middleware only understand JSON and URL-encoded data. When they encounter multipart/form-data, they don't know how to parse it — the request body remains empty or undefined.

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

// These only handle text-based formats
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.post('/upload', (req, res) => {
  console.log(req.body); // {} — empty! Can't parse multipart
  console.log(req.file); // undefined — no file handling
  res.send('Upload received');
});
Enter fullscreen mode Exit fullscreen mode

You need specialized middleware that understands the multipart format, can stream file data efficiently, and attaches parsed results to the request object.


What Multer Is

Multer is a Node.js middleware specifically designed for handling multipart/form-data. It integrates seamlessly with Express and provides:

  • File parsing: Reads the multipart stream and extracts files
  • Disk storage: Saves files to a folder on your server
  • Memory storage: Keeps files in memory (useful for processing before saving)
  • File filtering: Accept or reject files based on type, size, or extension
  • File naming: Control how uploaded files are named on disk
  • Field parsing: Extract regular text fields alongside files
  • Multiple file support: Handle single, multiple, or array uploads

How Multer Fits into the Request Lifecycle

Client sends multipart request
        ↓
Express receives the HTTP request
        ↓
Multer intercepts the request body
        ↓
Multer parses the multipart stream
  ├── Extracts text fields → attaches to req.body
  ├── Extracts file streams → saves to disk/memory
  └── Attaches file metadata → req.file / req.files
        ↓
Your route handler executes
  ├── req.body has text fields
  ├── req.file has single file info
  └── req.files has multiple file info
        ↓
Response sent back to client
Enter fullscreen mode Exit fullscreen mode

Installing Multer

npm install multer
Enter fullscreen mode Exit fullscreen mode

Handling Single File Upload

Step 1: Configure Multer

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

// Basic Multer setup — stores files in 'uploads/' folder
const upload = multer({ dest: 'uploads/' });

// The 'dest' option tells Multer where to save files
// Files are saved with random names (no extension) by default
Enter fullscreen mode Exit fullscreen mode

Step 2: Create the Upload Route

// 'avatar' is the field name from the HTML form
app.post('/profile', upload.single('avatar'), (req, res) => {
  // req.file contains the uploaded file metadata
  console.log(req.file);

  // req.body contains any text fields from the form
  console.log(req.body);

  res.json({
    message: 'File uploaded successfully',
    file: req.file
  });
});
Enter fullscreen mode Exit fullscreen mode

Step 3: The HTML Form

<form action="/profile" method="POST" enctype="multipart/form-data">
  <input type="text" name="username" placeholder="Your name" />
  <input type="file" name="avatar" accept="image/*" />
  <button type="submit">Upload</button>
</form>
Enter fullscreen mode Exit fullscreen mode

Critical: The form must have enctype="multipart/form-data" or the browser won't send files properly.

What req.file Contains

After a successful upload, req.file looks like this:

{
  fieldname: 'avatar',        // The form field name
  originalname: 'photo.jpg',  // Original filename on user's device
  encoding: '7bit',           // Encoding type
  mimetype: 'image/jpeg',     // Detected MIME type
  destination: 'uploads/',      // Folder where file was saved
  filename: 'a1b2c3d4e5f6',   // Random name generated by Multer
  path: 'uploads/a1b2c3d4e5f6', // Full path to the saved file
  size: 245678                // File size in bytes
}
Enter fullscreen mode Exit fullscreen mode

Complete Single Upload Example

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

const app = express();

// Ensure uploads directory exists
const uploadDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadDir)) {
  fs.mkdirSync(uploadDir);
}

// Configure Multer
const upload = multer({ dest: uploadDir });

// Single file upload endpoint
app.post('/upload/single', upload.single('document'), (req, res) => {
  if (!req.file) {
    return res.status(400).json({ error: 'No file uploaded' });
  }

  res.json({
    message: 'Upload successful',
    originalName: req.file.originalname,
    savedAs: req.file.filename,
    size: `${(req.file.size / 1024).toFixed(2)} KB`
  });
});

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});
Enter fullscreen mode Exit fullscreen mode

Handling Multiple File Uploads

Multiple Files, Same Field

Use upload.array() when a form allows selecting multiple files from one input:

<!-- Allow selecting multiple files -->
<input type="file" name="photos" multiple accept="image/*" />
Enter fullscreen mode Exit fullscreen mode
// Handle up to 5 files from the 'photos' field
app.post('/upload/gallery', upload.array('photos', 5), (req, res) => {
  // req.files is an array of file objects
  console.log(`Received ${req.files.length} files`);

  req.files.forEach(file => {
    console.log(`- ${file.originalname} (${file.size} bytes)`);
  });

  res.json({
    message: `${req.files.length} files uploaded`,
    files: req.files.map(f => ({
      name: f.originalname,
      savedAs: f.filename
    }))
  });
});
Enter fullscreen mode Exit fullscreen mode

Multiple Files, Different Fields

Use upload.fields() when different inputs have different purposes:

<form action="/upload/product" method="POST" enctype="multipart/form-data">
  <input type="text" name="productName" placeholder="Product name" />
  <input type="file" name="thumbnail" accept="image/*" />
  <input type="file" name="gallery" multiple accept="image/*" />
  <input type="file" name="manual" accept=".pdf" />
  <button type="submit">Create Product</button>
</form>
Enter fullscreen mode Exit fullscreen mode
// Define fields with name and maxCount
const productUpload = upload.fields([
  { name: 'thumbnail', maxCount: 1 },    // Exactly 1 thumbnail
  { name: 'gallery', maxCount: 10 },      // Up to 10 gallery images
  { name: 'manual', maxCount: 1 }         // 1 PDF manual
]);

app.post('/upload/product', productUpload, (req, res) => {
  // req.files is an object with arrays, keyed by field name
  console.log(req.files);

  const thumbnail = req.files['thumbnail']?.[0];
  const gallery = req.files['gallery'] || [];
  const manual = req.files['manual']?.[0];

  res.json({
    productName: req.body.productName,
    thumbnail: thumbnail?.originalname,
    galleryCount: gallery.length,
    manual: manual?.originalname
  });
});
Enter fullscreen mode Exit fullscreen mode

What req.files Contains

For upload.array():

[
  { fieldname: 'photos', originalname: '1.jpg', filename: 'abc123', ... },
  { fieldname: 'photos', originalname: '2.jpg', filename: 'def456', ... }
]
Enter fullscreen mode Exit fullscreen mode

For upload.fields():

{
  thumbnail: [{ ... }],
  gallery: [{ ... }, { ... }],
  manual: [{ ... }]
}
Enter fullscreen mode Exit fullscreen mode

Storage Configuration Basics

The default dest option saves files with random names and no extensions. For real applications, you need better control.

Disk Storage with Custom Filenames

const storage = multer.diskStorage({
  // Where to save files
  destination: (req, file, cb) => {
    cb(null, 'uploads/');
  },

  // How to name the saved files
  filename: (req, file, cb) => {
    // Keep original extension, add timestamp to prevent collisions
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
    const extension = path.extname(file.originalname);
    cb(null, file.fieldname + '-' + uniqueSuffix + extension);
  }
});

const upload = multer({ storage: storage });
Enter fullscreen mode Exit fullscreen mode

Resulting filenames:

  • avatar-1685123456789-123456789.jpg
  • document-1685123456790-987654321.pdf

File Filtering (Accept Only Images)

const imageFilter = (req, file, cb) => {
  // Accept only image files
  if (file.mimetype.startsWith('image/')) {
    cb(null, true); // Accept
  } else {
    cb(new Error('Only image files are allowed!'), false); // Reject
  }
};

const upload = multer({ 
  storage: storage,
  fileFilter: imageFilter,
  limits: { fileSize: 5 * 1024 * 1024 } // 5MB max
});
Enter fullscreen mode Exit fullscreen mode

Minimal but Practical Storage Setup

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

const app = express();

// Minimal, practical storage configuration
const storage = multer.diskStorage({
  destination: 'uploads/',
  filename: (req, file, cb) => {
    const ext = path.extname(file.originalname);
    cb(null, `${Date.now()}-${file.fieldname}${ext}`);
  }
});

const upload = multer({ 
  storage,
  limits: { fileSize: 10 * 1024 * 1024 } // 10MB limit
});
Enter fullscreen mode Exit fullscreen mode

Serving Uploaded Files

Uploading files is only half the job. Users need to access them afterward.

Static File Serving

The simplest approach: tell Express to serve the uploads folder as static files.

// Serve files from the 'uploads' directory
// Access via: http://localhost:3000/uploads/filename.jpg
app.use('/uploads', express.static('uploads'));
Enter fullscreen mode Exit fullscreen mode

Controlled File Serving

For security, you might want to check permissions before serving files:

const path = require('path');
const fs = require('fs');

app.get('/files/:filename', (req, res) => {
  const filename = req.params.filename;

  // Prevent directory traversal attacks
  const safePath = path.join(__dirname, 'uploads', path.basename(filename));

  // Check if file exists
  if (!fs.existsSync(safePath)) {
    return res.status(404).json({ error: 'File not found' });
  }

  // Optional: check if user owns this file (add your auth logic)

  res.sendFile(safePath);
});
Enter fullscreen mode Exit fullscreen mode

Upload + Serve Complete Example

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

const app = express();

// Setup
const uploadDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir);

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

const upload = multer({ storage });

// Upload endpoint
app.post('/upload', upload.single('file'), (req, res) => {
  if (!req.file) {
    return res.status(400).json({ error: 'No file provided' });
  }

  const fileUrl = `/uploads/${req.file.filename}`;

  res.json({
    message: 'Upload successful',
    fileUrl: fileUrl,
    originalName: req.file.originalname,
    size: req.file.size
  });
});

// Serve uploaded files
app.use('/uploads', express.static(uploadDir));

// List all uploaded files
app.get('/files', (req, res) => {
  fs.readdir(uploadDir, (err, files) => {
    if (err) return res.status(500).json({ error: 'Cannot read directory' });

    const fileList = files.map(name => ({
      name,
      url: `/uploads/${name}`
    }));

    res.json(fileList);
  });
});

app.listen(3000, () => {
  console.log('Server: http://localhost:3000');
  console.log('Upload: POST /upload (multipart, field: "file")');
  console.log('List: GET /files');
  console.log('Access: GET /uploads/<filename>');
});
Enter fullscreen mode Exit fullscreen mode

Upload Lifecycle Summary

┌─────────────────┐
│  User selects   │
│  file in browser│
└────────┬────────┘
         ↓
┌─────────────────┐
│ Form enctype=   │
│ multipart/form  │
└────────┬────────┘
         ↓
┌─────────────────┐
│ Browser encodes │
│ file into parts │
└────────┬────────┘
         ↓
┌─────────────────┐
│ HTTP POST with  │
│ boundary parts  │
└────────┬────────┘
         ↓
┌─────────────────┐
│ Express receives│
│ raw byte stream │
└────────┬────────┘
         ↓
┌─────────────────┐
│ Multer parses   │
│ multipart body  │
│ ├── text → body │
│ └── files → disk│
└────────┬────────┘
         ↓
┌─────────────────┐
│ Route handler   │
│ accesses file   │
│ metadata        │
└────────┬────────┘
         ↓
┌─────────────────┐
│ Response sent   │
│ with file info  │
└─────────────────┘
Enter fullscreen mode Exit fullscreen mode

Common Pitfalls

Pitfall Solution
Missing enctype Always include enctype="multipart/form-data" on forms
Wrong field name The string in upload.single('avatar') must match the input's name attribute
No file size limits Set limits: { fileSize: ... } to prevent huge uploads
No file type filter Use fileFilter to reject dangerous file types
Memory overload Use disk storage for large files; memory storage only for small, temporary processing
Filename collisions Use timestamps or UUIDs in filename function

Summary

Concept What It Means
Multipart form-data A format that packages text fields and binary files into a single HTTP request with boundary separators
Why middleware is needed Express can't parse multipart streams; specialized middleware (Multer) handles the complex parsing and file streaming
Multer Node.js middleware that parses multipart requests, saves files to disk or memory, and attaches metadata to req.file / req.files
Single upload upload.single('fieldname')req.file
Multiple uploads upload.array('fieldname', maxCount)req.files (array)
Mixed fields upload.fields([{name, maxCount}])req.files (object)
Storage config diskStorage controls destination path and filename generation
Serving files express.static() makes uploaded files accessible via URL

File uploads don't have to be complicated. With Multer handling the multipart parsing and file streaming, you can focus on what matters: building features that use those files, whether it's profile pictures, document stores, or media galleries. Start with disk storage, add filters for security, and serve files statically — that's the foundation everything else builds on.

Remember: Multipart is just a package format. Multer is the postal worker who opens the package, sorts the contents, and hands them to you neatly labeled. Without it, you're trying to unpack a sealed box with your bare hands.

Top comments (0)