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');
}