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.

Nexsaar Technologies - React JS, Node JS, Odoo, Salesforce, Java Development Services

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:

  1. Create a free account at cloudinary.com
  2. Login to the dashboard
  3. Copy the Cloud Name, API Key, and API Secret
  4. 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:

TypeFormats
ImagesJPG, PNG, WEBP, GIF, SVG
VideosMP4, MOV, AVI, WEBM
DocumentsPDF

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:

  1. Validate file types – Only allow specific formats
  2. Limit file size – Prevent large uploads (e.g., 10MB max)
  3. Protect upload routes – Use authentication middleware
  4. Use environment variables – Never hardcode credentials
  5. Enable signed uploads – For sensitive files, use signed URLs
  6. Sanitize filenames – Prevent injection attacks
  7. 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! 🚀

Useful Resources

More articles

Infinite scrolling with intersection observer

Explore modern, secure, and developer-friendly authentication approaches tailored for JavaScript applications. Learn how to simplify login, authorization, and session management without unnecessary complexity.

Read more

Modern Authentication Solutions for JavaScript Developers

Explore modern, secure, and developer-friendly authentication approaches tailored for JavaScript applications. Learn how to simplify login, authorization, and session management without unnecessary complexity.

Read more