File Uploads with Multer

Difficulty: Intermediate

HTTP was originally designed for text data, but modern web applications frequently need to handle file uploads: profile pictures, documents, spreadsheets, videos, and more. The standard way to send files over HTTP is using the multipart/form-data content type, which can include both text fields and binary file data in a single request. Express.json() and express.urlencoded() cannot parse multipart data - you need a specialized library.

Multer is the most popular file upload middleware for Express. It processes multipart/form-data requests and makes uploaded files available on req.file (for single uploads) or req.files (for multiple uploads). Multer handles the complex task of reading binary data from the multipart stream, buffering it, and either storing it on disk or keeping it in memory as a Buffer.

Multer provides two storage engines. DiskStorage saves files directly to the file system, where you can configure the destination directory and filename. MemoryStorage keeps files in memory as Buffer objects on req.file.buffer, which is useful when you want to process the file (resize images, upload to cloud storage like S3) before saving it. For production applications that use cloud storage, MemoryStorage is typically preferred because you never need to write temporary files to disk.

File validation is critical for security. Without validation, users could upload executable files, gigabyte-sized archives, or files with malicious content. Multer provides a fileFilter function that lets you reject files based on their mimetype, extension, or other criteria. The limits option restricts file size, number of files, and field name length. Always validate both the file type and file size on the server - client-side validation can be bypassed.

Multer offers different methods for different upload scenarios: upload.single('fieldName') for a single file, upload.array('fieldName', maxCount) for multiple files from the same field, upload.fields([{name, maxCount}]) for files from different fields, and upload.none() for multipart requests with no files (just text fields).

Code examples

Single File Upload

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

// Configure disk storage
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, path.join(__dirname, 'uploads'));
  },
  filename: (req, file, cb) => {
    // Create unique filename: timestamp-originalname
    const uniqueName = `${Date.now()}-${file.originalname}`;
    cb(null, uniqueName);
  }
});

// File filter - only allow images
const fileFilter = (req, file, cb) => {
  const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
  if (allowedTypes.includes(file.mimetype)) {
    cb(null, true);  // Accept file
  } else {
    cb(new Error('Only image files are allowed'), false); // Reject
  }
};

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

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

  res.json({
    message: 'File uploaded successfully',
    file: {
      filename: req.file.filename,
      originalName: req.file.originalname,
      mimetype: req.file.mimetype,
      size: req.file.size,
      path: req.file.path
    }
  });
});

// Handle multer errors
app.use((err, req, res, next) => {
  if (err instanceof multer.MulterError) {
    if (err.code === 'LIMIT_FILE_SIZE') {
      return res.status(400).json({ error: 'File too large. Max size is 5MB.' });
    }
    return res.status(400).json({ error: err.message });
  }
  if (err.message === 'Only image files are allowed') {
    return res.status(400).json({ error: err.message });
  }
  next(err);
});

app.listen(3000, () => console.log('Upload server on port 3000'));

upload.single('avatar') processes a single file from the form field named 'avatar'. The file is available on req.file with properties like filename, originalname, mimetype, size, and path. Multer errors (size limit, wrong type) are caught by the error middleware.

Multiple File Uploads

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

const upload = multer({
  storage: multer.memoryStorage(), // Store in memory as Buffer
  limits: { fileSize: 10 * 1024 * 1024 } // 10MB per file
});

// Multiple files from the same field
app.post('/api/upload/photos', upload.array('photos', 5), (req, res) => {
  if (!req.files || req.files.length === 0) {
    return res.status(400).json({ error: 'No files uploaded' });
  }

  const fileInfo = req.files.map(file => ({
    originalName: file.originalname,
    mimetype: file.mimetype,
    size: file.size,
    // file.buffer contains the raw file data
    bufferSize: file.buffer.length
  }));

  res.json({
    message: `${req.files.length} files uploaded`,
    files: fileInfo
  });
});

// Multiple fields with different file types
app.post('/api/upload/profile', upload.fields([
  { name: 'avatar', maxCount: 1 },
  { name: 'resume', maxCount: 1 },
  { name: 'portfolio', maxCount: 5 }
]), (req, res) => {
  res.json({
    avatar: req.files['avatar'] ? req.files['avatar'][0].originalname : null,
    resume: req.files['resume'] ? req.files['resume'][0].originalname : null,
    portfolioCount: req.files['portfolio'] ? req.files['portfolio'].length : 0,
    // Text fields from the multipart form
    name: req.body.name,
    bio: req.body.bio
  });
});

app.listen(3000, () => console.log('Server on port 3000'));

upload.array('photos', 5) accepts up to 5 files from the 'photos' field. upload.fields() handles files from multiple named fields. With memoryStorage, files are available as Buffers on file.buffer, ready to be processed or uploaded to cloud storage.

Upload to Cloud Storage (S3 Pattern)

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

// Use memory storage for cloud uploads
const upload = multer({
  storage: multer.memoryStorage(),
  limits: { fileSize: 5 * 1024 * 1024 },
  fileFilter: (req, file, cb) => {
    const allowed = ['image/jpeg', 'image/png', 'application/pdf'];
    if (allowed.includes(file.mimetype)) cb(null, true);
    else cb(new Error('Invalid file type'), false);
  }
});

// Simulated cloud upload function
async function uploadToCloud(buffer, filename, mimetype) {
  // In real code, this would upload to S3, GCS, etc.
  console.log(`Uploading ${filename} (${mimetype}, ${buffer.length} bytes)`);
  return {
    url: `https://storage.example.com/uploads/${Date.now()}-${filename}`,
    key: `uploads/${Date.now()}-${filename}`
  };
}

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

    // Upload the buffer to cloud storage
    const result = await uploadToCloud(
      req.file.buffer,
      req.file.originalname,
      req.file.mimetype
    );

    res.json({
      message: 'File uploaded to cloud',
      url: result.url,
      originalName: req.file.originalname,
      size: req.file.size
    });
  } catch (error) {
    res.status(500).json({ error: 'Upload failed' });
  }
});

app.listen(3000, () => console.log('Server on port 3000'));

memoryStorage keeps the file as a Buffer, which can be passed directly to cloud storage SDKs (AWS S3 putObject, Google Cloud Storage upload). This avoids writing temporary files to disk. The async handler processes the upload and returns the cloud URL.

Key points

Concepts covered

Multer, multipart/form-data, Single Upload, Multiple Uploads, Storage Engines, File Validation