File Uploads (Multer) · learncode.live

What is Multer?

Multer is a Node.js middleware for handling multipart/form-data - the format used for file uploads.

Browser                              Server
  │                                    │
  │── POST /upload ───────────────────→│
  │   Content-Type: multipart/form-data│
  │   ┌─────────────────────────┐      │
  │   │ name: "Alice"           │      │
  │   │ avatar: (binary file)   │──────→│ Multer parses
  │   │ resume: (binary file)   │      │ req.body → { name: "Alice" }
  │   └─────────────────────────┘      │ req.files → [{...file data}]
  │                                    │

Installation

npm install multer

Basic Setup

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

// Configure storage
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, 'uploads/');
  },
  filename: (req, file, cb) => {
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
    cb(null, uniqueSuffix + path.extname(file.originalname));
  }
});

// File filter
const fileFilter = (req, file, cb) => {
  const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
  if (allowedTypes.includes(file.mimetype)) {
    cb(null, true);
  } else {
    cb(new Error('Only JPEG, PNG, and GIF files are allowed'), false);
  }
};

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

module.exports = upload;

Upload Routes

Single File Upload

const upload = require('./middleware/upload');

// Single file - field name "avatar"
app.post('/upload/avatar', upload.single('avatar'), (req, res) => {
  console.log(req.file); // Uploaded file info
  console.log(req.body); // Other form fields

  res.json({
    success: true,
    message: 'File uploaded',
    file: req.file.filename
  });
});

Multiple Files (Same Field)

// Multiple files - same field "gallery", max 5
app.post('/upload/gallery', upload.array('gallery', 5), (req, res) => {
  res.json({
    success: true,
    files: req.files.map(f => f.filename)
  });
});

Multiple Fields

// Different fields with different limits
const cpUpload = upload.fields([
  { name: 'avatar', maxCount: 1 },
  { name: 'gallery', maxCount: 8 },
  { name: 'documents', maxCount: 3 }
]);

app.post('/upload/profile', cpUpload, (req, res) => {
  console.log(req.files.avatar);     // Array of 1 file
  console.log(req.files.gallery);    // Array of up to 8 files
  console.log(req.files.documents);  // Array of up to 3 files
  console.log(req.body);             // Text fields

  res.json({ success: true });
});

req.file / req.files Properties

{
  fieldname: 'avatar',         // Form field name
  originalname: 'profile.jpg', // Original file name
  encoding: '7bit',            // Encoding type
  mimetype: 'image/jpeg',      // MIME type
  destination: 'uploads/',     // Storage directory
  filename: '1689000000-123.jpg', // Generated filename
  path: 'uploads/1689000000-123.jpg', // Full path
  size: 24567                  // File size in bytes
}

Storage Engines

Disk Storage (Default)

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    // Create date-based subdirectories
    const date = new Date();
    const dir = `uploads/${date.getFullYear()}/${date.getMonth() + 1}`;
    fs.mkdirSync(dir, { recursive: true });
    cb(null, dir);
  },
  filename: (req, file, cb) => {
    // Preserve original extension, add unique prefix
    const name = file.originalname.replace(/\s+/g, '-').toLowerCase();
    cb(null, `${Date.now()}-${name}`);
  }
});

Memory Storage (For Cloud Uploads)

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

app.post('/upload', upload.single('file'), async (req, res) => {
  // req.file.buffer contains the file as a Buffer
  // Upload directly to S3, Cloudinary, etc.
  const result = await cloudinary.uploader.upload_stream({
    resource_type: 'auto',
    public_id: `uploads/${Date.now()}`
  }, (error, result) => {
    if (error) return res.status(500).json({ error: 'Upload failed' });
    res.json({ url: result.secure_url });
  }).end(req.file.buffer);
});

Cloud Storage (S3 Example)

const multer = require('multer');
const multerS3 = require('multer-s3');
const { S3Client } = require('@aws-sdk/client-s3');

const s3 = new S3Client({
  region: process.env.AWS_REGION,
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY,
    secretAccessKey: process.env.AWS_SECRET_KEY
  }
});

const upload = multer({
  storage: multerS3({
    s3,
    bucket: 'myapp-uploads',
    acl: 'public-read',
    metadata: (req, file, cb) => {
      cb(null, { fieldName: file.fieldname });
    },
    key: (req, file, cb) => {
      cb(null, `uploads/${Date.now()}-${file.originalname}`);
    }
  })
});

File Validation

// Upload middleware with validation
const upload = multer({
  storage,
  limits: { fileSize: 5 * 1024 * 1024 },
  fileFilter: (req, file, cb) => {
    // Check file type
    const imageTypes = ['image/jpeg', 'image/png', 'image/webp'];
    const docTypes = ['application/pdf', 'application/msword'];
    const allAllowed = [...imageTypes, ...docTypes];

    if (!allAllowed.includes(file.mimetype)) {
      return cb(new Error(`Invalid file type: ${file.mimetype}`), false);
    }

    // Check file name length
    if (file.originalname.length > 100) {
      return cb(new Error('File name too long'), false);
    }

    cb(null, true);
  }
});

// Handle multer errors
app.post('/upload', (req, res) => {
  upload.single('file')(req, res, (err) => {
    if (err instanceof multer.MulterError) {
      if (err.code === 'LIMIT_FILE_SIZE') {
        return res.status(400).json({ error: 'File too large. Max 5MB.' });
      }
      if (err.code === 'LIMIT_FILE_COUNT') {
        return res.status(400).json({ error: 'Too many files.' });
      }
      if (err.code === 'LIMIT_UNEXPECTED_FILE') {
        return res.status(400).json({ error: 'Unexpected file field.' });
      }
      return res.status(400).json({ error: err.message });
    }

    if (err) {
      return res.status(400).json({ error: err.message });
    }

    res.json({ success: true, file: req.file.filename });
  });
});

HTML Form Example

<!-- Single file -->
<form action="/upload/avatar" method="POST" enctype="multipart/form-data">
  <input type="text" name="name" placeholder="Your name" />
  <input type="file" name="avatar" accept="image/*" />
  <button type="submit">Upload</button>
</form>

<!-- Multiple files -->
<form action="/upload/gallery" method="POST" enctype="multipart/form-data">
  <input type="file" name="gallery" multiple accept="image/*" />
  <button type="submit">Upload Gallery</button>
</form>

Upload with Fetch API (Frontend)

async function uploadFile(file) {
  const formData = new FormData();
  formData.append('avatar', file);
  formData.append('name', 'Alice');

  const res = await fetch('/upload/avatar', {
    method: 'POST',
    body: formData // No Content-Type header - browser sets it with boundary
  });

  return res.json();
}

// Upload multiple files
async function uploadGallery(files) {
  const formData = new FormData();
  files.forEach(f => formData.append('gallery', f));

  const res = await fetch('/upload/gallery', {
    method: 'POST',
    body: formData
  });

  return res.json();
}

Serving Uploaded Files

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

// Now files are accessible at:
// http://localhost:3000/uploads/1689000000-123.jpg

Cleanup Old Files

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

// Schedule cleanup of files older than 24 hours
setInterval(() => {
  const uploadDir = 'uploads/';
  const now = Date.now();

  fs.readdir(uploadDir, (err, files) => {
    if (err) return;

    files.forEach(file => {
      const filePath = path.join(uploadDir, file);
      fs.stat(filePath, (err, stats) => {
        if (err) return;
        if (now - stats.mtimeMs > 24 * 60 * 60 * 1000) {
          fs.unlink(filePath, () => {});
        }
      });
    });
  });
}, 60 * 60 * 1000); // Run every hour

Key Takeaways

  • Multer parses multipart/form-data - use for file uploads
  • Configure storage (disk, memory, S3), limits, and fileFilter
  • Use upload.single('field') for one file, upload.array('field', n) for multiple
  • Handle MulterError (file too large, too many files) separately from other errors
  • Always validate file types and sizes before saving
  • Store files in date-based subdirectories for organization
  • Serve uploaded files via express.static('uploads')
  • For cloud storage, use memoryStorage and upload the buffer to S3/Cloudinary
Courses