Node.js

File Uploads – Multer, Cloudinary & Static Files

Handle file uploads with multer: configure disk storage, validate MIME types, accept multiple files, stream uploads to Cloudinary, and serve a public uploads directory.

Install: multer parses multipart/form-data. cloudinary and streamifier are needed for buffer-to-stream Cloudinary uploads.
📄terminal
BASH
npm install multer cloudinary streamifier
Disk Storage Config: Use diskStorage to control the upload folder and generate unique filenames. Files are saved directly to the server.
📄src/config/multer.js
JS
const multer = require('multer');
const path   = require('path');
const crypto = require('crypto');

// ── Disk storage ────────────────────────────────────
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, 'public/uploads/');   // must exist or be created
  },
  filename: (req, file, cb) => {
    const unique = crypto.randomBytes(8).toString('hex');
    const ext    = path.extname(file.originalname);
    cb(null, `${Date.now()}-${unique}${ext}`);
  },
});

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

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

// ── Multiple files (up to 5) ────────────────────────
const uploadMultiple = multer({
  storage,
  fileFilter: imageFilter,
  limits: { fileSize: 5 * 1024 * 1024 },
});

module.exports = { uploadImage, uploadMultiple };
Document Filter: Accept PDF and Office documents alongside images by extending the allowed MIME type list.
📄src/config/multerDocs.js
JS
const multer = require('multer');

const ALLOWED_TYPES = new Set([
  'image/jpeg', 'image/png', 'image/webp',
  'application/pdf',
  'application/msword',
  'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
]);

const docFilter = (req, file, cb) => {
  ALLOWED_TYPES.has(file.mimetype)
    ? cb(null, true)
    : cb(new Error(`Unsupported file type: ${file.mimetype}`), false);
};

const uploadDoc = multer({
  storage: multer.memoryStorage(),   // keep in buffer for cloud upload
  fileFilter: docFilter,
  limits: { fileSize: 10 * 1024 * 1024 },  // 10 MB
});

module.exports = uploadDoc;
Multiple Files Route: Use .array(fieldName, maxCount) to accept multiple files under the same field, or .fields([]) for different field names.
📄src/routes/uploadRoutes.js
JS
const express  = require('express');
const router   = express.Router();
const { uploadImage, uploadMultiple } = require('../config/multer');
const uploadCtrl = require('../controllers/uploadController');

// Single avatar upload
router.post('/avatar',
  uploadImage.single('avatar'),
  uploadCtrl.uploadAvatar
);

// Gallery – up to 5 images
router.post('/gallery',
  uploadMultiple.array('images', 5),
  uploadCtrl.uploadGallery
);

// Different fields: cover + screenshots
router.post('/product',
  uploadMultiple.fields([
    { name: 'cover',       maxCount: 1 },
    { name: 'screenshots', maxCount: 8 },
  ]),
  uploadCtrl.uploadProduct
);

module.exports = router;
Upload to Cloudinary: Use memoryStorage so the file stays as a Buffer, then stream it to Cloudinary without touching the disk.
📄src/config/cloudinary.js
JS
const cloudinary  = require('cloudinary').v2;
const streamifier = require('streamifier');

cloudinary.config({
  cloud_name: process.env.CLOUDINARY_NAME,
  api_key:    process.env.CLOUDINARY_KEY,
  api_secret: process.env.CLOUDINARY_SECRET,
});

/**
 * Upload a Buffer to Cloudinary and return the result.
 * @param {Buffer} buffer
 * @param {string} folder  - e.g. 'avatars' or 'gallery'
 * @returns {Promise<object>} Cloudinary upload result
 */
const uploadToCloudinary = (buffer, folder = 'uploads') =>
  new Promise((resolve, reject) => {
    const stream = cloudinary.uploader.upload_stream(
      { folder, resource_type: 'auto' },
      (error, result) => (error ? reject(error) : resolve(result))
    );
    streamifier.createReadStream(buffer).pipe(stream);
  });

module.exports = { uploadToCloudinary };
Upload Controller: Retrieve req.file or req.files, stream to Cloudinary, and return the secure URL for storage in the database.
📄src/controllers/uploadController.js
JS
const { uploadToCloudinary } = require('../config/cloudinary');

const uploadAvatar = async (req, res) => {
  try {
    if (!req.file) return res.status(400).json({ message: 'No file uploaded' });

    const result = await uploadToCloudinary(req.file.buffer, 'avatars');

    // TODO: save result.secure_url to the user record in DB
    res.json({
      url:       result.secure_url,
      public_id: result.public_id,
    });
  } catch (err) {
    res.status(500).json({ message: err.message });
  }
};

const uploadGallery = async (req, res) => {
  try {
    if (!req.files?.length) {
      return res.status(400).json({ message: 'No files uploaded' });
    }

    const uploads = await Promise.all(
      req.files.map((f) => uploadToCloudinary(f.buffer, 'gallery'))
    );

    res.json(uploads.map((r) => ({ url: r.secure_url, id: r.public_id })));
  } catch (err) {
    res.status(500).json({ message: err.message });
  }
};

module.exports = { uploadAvatar, uploadGallery };
Serving Static Files: Use express.static() to expose an uploads/ directory. Uploaded files become accessible at /uploads/filename.jpg.
📄server.js (static middleware)
JS
const path = require('path');

// Serve public/uploads at /uploads
app.use('/uploads', express.static(path.join(__dirname, 'public/uploads')));

// Optional: serve a full public folder
app.use(express.static(path.join(__dirname, 'public')));

// Ensure upload directory exists at startup
const fs = require('fs');
const uploadDir = path.join(__dirname, 'public/uploads');
if (!fs.existsSync(uploadDir)) {
  fs.mkdirSync(uploadDir, { recursive: true });
  console.log('Created uploads directory');
}