How to Upload Images, Videos, and PDFs to Cloudinary Using MERN Stack
MERN Stack Developer
Learn how to build a complete file upload system using the MERN stack with Cloudinary. This comprehensive guide covers uploading images, videos, and PDFs, handling security, and implementing best practices for scalable media management.
Introduction
In modern web development, handling media files such as images, videos, and PDF documents is a core requirement for most applications. Whether it is a profile picture, product image, promotional video, or invoice PDF, file uploads must be secure, scalable, and fast.
Storing files directly on a server creates several issues such as limited storage, slow performance, security risks, and poor scalability. As applications grow, managing these files becomes increasingly difficult.
Cloudinary solves these problems by providing a cloud-based media management platform. In this guide, we will build a complete file upload system using the MERN stack (MongoDB, Express, React, Node.js) with Cloudinary.

What is Cloudinary?
Cloudinary is a cloud-based service that allows developers to upload, store, manage, optimize, and deliver media assets. It supports images, videos, and raw files such as PDFs.
🔗 Official Website: cloudinary.com
Cloudinary provides:
- ✅ Automatic media optimization
- ✅ Fast delivery using global CDN
- ✅ Support for multiple file formats
- ✅ Secure and scalable storage
- ✅ Easy integration with Node.js and React
Why Use Cloudinary in MERN Stack?
Using Cloudinary in a MERN stack application offers several advantages:
- Reduces server load – Files are stored on Cloudinary's cloud infrastructure
- Improves application performance – Global CDN ensures fast delivery
- Eliminates manual file management – No need to manage local storage
- Supports large media files – Handle videos and high-resolution images effortlessly
- Enhances security – Built-in security features and access control
Cloudinary Account Setup
To use Cloudinary, follow these steps:
- Create a free account at cloudinary.com
- Login to the dashboard
- Copy the Cloud Name, API Key, and API Secret
- Store credentials securely in environment variables
⚠️ Security Note: Never expose Cloudinary credentials in frontend code.
Create a .env file in your backend directory:
CLOUDINARY_CLOUD_NAME=your_cloud_name
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_API_SECRET=your_api_secret
Installing Required Packages
We need the following packages to handle file uploads:
- cloudinary – Official Cloudinary SDK
- multer – Middleware for handling
multipart/form-data - multer-storage-cloudinary – Cloudinary storage engine for Multer
Install dependencies in your Node.js backend:
npm install cloudinary multer multer-storage-cloudinary
📦 Package Links:
Cloudinary Configuration in Node.js
The Cloudinary SDK must be configured in the backend. Create a separate configuration file to keep code clean and reusable.
Create config/cloudinary.js:
import { v2 as cloudinary } from 'cloudinary'
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
})
export default cloudinary
This configuration uses environment variables to securely connect to your Cloudinary account.
Multer Setup for Cloudinary Storage
Multer handles multipart/form-data and processes files before sending them to Cloudinary. Using multer-storage-cloudinary allows direct uploads to Cloudinary.
Setting resource_type to "auto" enables support for images, videos, and PDFs.
Create middleware/upload.js:
import multer from 'multer'
import { CloudinaryStorage } from 'multer-storage-cloudinary'
import cloudinary from '../config/cloudinary.js'
const storage = new CloudinaryStorage({
cloudinary,
params: {
folder: 'mern_uploads',
resource_type: 'auto', // Supports images, videos, and PDFs
allowed_formats: ['jpg', 'jpeg', 'png', 'pdf', 'mp4', 'mov'],
},
})
export const upload = multer({
storage,
limits: {
fileSize: 10 * 1024 * 1024, // 10MB limit
},
})
🔗 Documentation: Multer Storage Cloudinary
Creating Express Upload API
The upload API receives files from the client and sends them to Cloudinary. After upload, Cloudinary returns a secure URL which can be stored in MongoDB.
Create routes/upload.js:
import express from 'express'
import { upload } from '../middleware/upload.js'
const router = express.Router()
router.post('/upload', upload.single('file'), (req, res) => {
try {
if (!req.file) {
return res.status(400).json({
success: false,
message: 'No file uploaded',
})
}
res.status(200).json({
success: true,
fileUrl: req.file.path,
fileType: req.file.mimetype,
fileName: req.file.originalname,
cloudinaryId: req.file.filename,
})
} catch (error) {
res.status(500).json({
success: false,
message: 'Upload failed',
error: error.message,
})
}
})
export default router
Then register the route in your main server.js:
import express from 'express'
import uploadRoutes from './routes/upload.js'
const app = express()
app.use('/api', uploadRoutes)
app.listen(5000, () => {
console.log('Server running on port 5000')
})
Uploading Files from React Frontend
On the frontend, files are uploaded using a file input and FormData. Axios is used to send multipart/form-data requests.
Create a React component for file upload:
import { useState } from 'react'
import axios from 'axios'
function FileUpload() {
const [selectedFile, setSelectedFile] = useState(null)
const [uploadedUrl, setUploadedUrl] = useState('')
const [loading, setLoading] = useState(false)
const handleFileChange = (e) => {
setSelectedFile(e.target.files[0])
}
const handleUpload = async () => {
if (!selectedFile) {
alert('Please select a file')
return
}
setLoading(true)
const formData = new FormData()
formData.append('file', selectedFile)
try {
const response = await axios.post(
'http://localhost:5000/api/upload',
formData,
{
headers: { 'Content-Type': 'multipart/form-data' },
},
)
setUploadedUrl(response.data.fileUrl)
alert('Upload successful!')
} catch (error) {
console.error('Upload error:', error)
alert('Upload failed')
} finally {
setLoading(false)
}
}
return (
<div>
<h2>Upload File to Cloudinary</h2>
<input type="file" onChange={handleFileChange} />
<button onClick={handleUpload} disabled={loading}>
{loading ? 'Uploading...' : 'Upload'}
</button>
{uploadedUrl && (
<div>
<p>File uploaded successfully!</p>
<a href={uploadedUrl} target="_blank" rel="noopener noreferrer">
View File
</a>
</div>
)}
</div>
)
}
export default FileUpload
Handling Images, Videos, and PDFs
Cloudinary automatically detects the uploaded file type. The same upload logic works for all file types.
Supported file formats include:
| Type | Formats |
|---|---|
| Images | JPG, PNG, WEBP, GIF, SVG |
| Videos | MP4, MOV, AVI, WEBM |
| Documents |
You can validate file types on the backend:
const allowedTypes = ['image/jpeg', 'image/png', 'video/mp4', 'application/pdf']
router.post('/upload', upload.single('file'), (req, res) => {
if (!allowedTypes.includes(req.file.mimetype)) {
return res.status(400).json({ error: 'Invalid file type' })
}
// Continue with upload
})
Storing File URLs in MongoDB
After uploading to Cloudinary, store the returned URL in MongoDB. This approach keeps the database lightweight and efficient.
Create a Mongoose schema:
import mongoose from 'mongoose'
const userSchema = new mongoose.Schema(
{
name: { type: String, required: true },
avatar: { type: String }, // Image URL
resumePdf: { type: String }, // PDF URL
videoUrl: { type: String }, // Video URL
cloudinaryId: { type: String }, // For deletion reference
},
{ timestamps: true },
)
export default mongoose.model('User', userSchema)
Example: Save uploaded file URL to database:
import User from '../models/User.js'
router.post('/profile-upload', upload.single('avatar'), async (req, res) => {
try {
const user = await User.findById(req.user.id)
user.avatar = req.file.path
user.cloudinaryId = req.file.filename
await user.save()
res.json({ success: true, avatarUrl: user.avatar })
} catch (error) {
res.status(500).json({ error: error.message })
}
})
Deleting Files from Cloudinary
When users delete their profile or remove files, also delete from Cloudinary to save storage:
import cloudinary from '../config/cloudinary.js'
router.delete('/delete-file/:cloudinaryId', async (req, res) => {
try {
await cloudinary.uploader.destroy(req.params.cloudinaryId)
res.json({ success: true, message: 'File deleted' })
} catch (error) {
res.status(500).json({ error: error.message })
}
})
🔗 Documentation: Cloudinary Delete Assets
Security Best Practices
File uploads must be handled securely to prevent misuse.
Best practices include:
- ✅ Validate file types – Only allow specific formats
- ✅ Limit file size – Prevent large uploads (e.g., 10MB max)
- ✅ Protect upload routes – Use authentication middleware
- ✅ Use environment variables – Never hardcode credentials
- ✅ Enable signed uploads – For sensitive files, use signed URLs
- ✅ Sanitize filenames – Prevent injection attacks
- ✅ Rate limiting – Prevent abuse with too many uploads
Example authentication middleware:
import jwt from 'jsonwebtoken'
const authMiddleware = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1]
if (!token) return res.status(401).json({ error: 'Unauthorized' })
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET)
req.user = decoded
next()
} catch (error) {
res.status(401).json({ error: 'Invalid token' })
}
}
// Protected upload route
router.post('/upload', authMiddleware, upload.single('file'), (req, res) => {
// Handle upload
})
Advanced Features
1. Image Transformation
Cloudinary can resize, crop, and optimize images automatically:
const transformedUrl = cloudinary.url(req.file.filename, {
width: 300,
height: 300,
crop: 'fill',
gravity: 'face',
quality: 'auto',
fetch_format: 'auto',
})
🔗 Documentation: Image Transformations
2. Multiple File Upload
Handle multiple files at once:
router.post('/upload-multiple', upload.array('files', 5), (req, res) => {
const fileUrls = req.files.map((file) => file.path)
res.json({ success: true, fileUrls })
})
3. Direct Upload from Frontend (Unsigned)
For public uploads, configure unsigned presets in Cloudinary dashboard.
Conclusion
Cloudinary provides a powerful and scalable solution for handling media uploads in MERN stack applications. By integrating Cloudinary with Multer, Express, and React, developers can easily manage images, videos, and PDFs.
Key Takeaways:
- ✅ Cloudinary eliminates server storage issues
- ✅ Multer + Cloudinary integration is straightforward
- ✅ Same upload logic works for images, videos, and PDFs
- ✅ Store only URLs in MongoDB for efficiency
- ✅ Security and validation are critical
This guide demonstrates a production-ready approach to file uploads that improves performance, security, and scalability.
Happy coding! 🚀