A common feature of many contemporary web applications is the ability to upload files, such as documents, videos, and images. In this tutorial, we'll demonstrate how to use Cloudinary, a potent media management platform, to create a robust file upload feature in a React (v18+) application. We'll also go over secure backend-signed uploads using Node.js (Express). Using functional components, React Hooks, and contemporary best practices, we'll create a reusable upload component that supports a variety of file types, displays a preview when feasible, tracks the upload process, and securely uploads media using a backend-generated signature.

What is Cloudinary?
Cloudinary is a cloud-based service for storing, optimizing, and delivering images, videos, and other media files. It simplifies media handling by providing:

  • Media upload and storage
  • CDN delivery and transformation
  • Automatic optimization and responsive images
  • Support for multiple media types

What Will We Build?
A full-stack app (React + Node.js) that:

  • Accepts images, videos, and documents as input
  • Shows previews for image/video types
  • Tracks upload progress
  • Generates a secure upload signature on the backend
  • Uploads securely to Cloudinary

Project Structure

 

cloudinary-react-upload/
├── client/            # React frontend
│   ├── src/
│   │   ├── components/FileUploader.jsx
│   │   ├── App.jsx
│   │   └── main.jsx
│   └── .env
├── server/            # Node.js backend
│   ├── index.js
│   └── .env
├── package.json (root - manages both client/server via scripts)

 

Step 1. Cloudinary Setup

  • Sign up at cloudinary.com
  • Go to your dashboard and note:
    • Cloud Name
    • API Key
    • API Secret
  • Navigate to Settings > Upload > Upload Presets
    • Create a new signed preset
    • Enable "Auto format" and "Auto resource type"

Backend .env (in server/.env)
CLOUD_NAME=your_cloud_name
CLOUD_API_KEY=your_api_key
CLOUD_API_SECRET=your_api_secret
UPLOAD_PRESET=your_signed_preset

Step 2: Backend Setup with Node.js (Express)
Install dependencies
cd server
npm init -y
npm install express dotenv cors cloudinary

server/index.js
import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import { v2 as cloudinary } from 'cloudinary';

dotenv.config();
const app = express();
app.use(cors());

cloudinary.config({
  cloud_name: process.env.CLOUD_NAME,
  api_key: process.env.CLOUD_API_KEY,
  api_secret: process.env.CLOUD_API_SECRET
});

app.get('/get-signature', (req, res) => {
  const timestamp = Math.floor(Date.now() / 1000);
  const signature = cloudinary.utils.api_sign_request(
    {
      timestamp,
      upload_preset: process.env.UPLOAD_PRESET,
    },
    process.env.CLOUD_API_SECRET
  );

  res.json({
    timestamp,
    signature,
    cloudName: process.env.CLOUD_NAME,
    apiKey: process.env.CLOUD_API_KEY,
    uploadPreset: process.env.UPLOAD_PRESET,
  });
});

const PORT = process.env.PORT || 4000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));


Run the backend:
node index.js

Step 3. React Frontend Setup (Vite)
Create project and install dependencies:
npm create vite@latest client -- --template react
cd client
npm install axios

Frontend .env (in client/.env)
VITE_API_URL=http://localhost:4000

Step 4. FileUploader Component (Secure Upload)

client/src/components/FileUploader.jsx

import { useState, useRef } from 'react';
import axios from 'axios';

const FileUploader = () => {
  const [file, setFile] = useState(null);
  const [previewUrl, setPreviewUrl] = useState(null);
  const [progress, setProgress] = useState(0);
  const [uploadedUrl, setUploadedUrl] = useState(null);
  const inputRef = useRef();

  const handleFileChange = (e) => {
    const selected = e.target.files[0];
    setFile(selected);
    setUploadedUrl(null);
    setProgress(0);

    if (selected?.type.startsWith('image') || selected?.type.startsWith('video')) {
      const url = URL.createObjectURL(selected);
      setPreviewUrl(url);
    } else {
      setPreviewUrl(null);
    }
  };

  const handleUpload = async () => {
    if (!file) return;

    try {
      const { data: signatureData } = await axios.get(`${import.meta.env.VITE_API_URL}/get-signature`);

      const formData = new FormData();
      formData.append('file', file);
      formData.append('api_key', signatureData.apiKey);
      formData.append('timestamp', signatureData.timestamp);
      formData.append('upload_preset', signatureData.uploadPreset);
      formData.append('signature', signatureData.signature);

      const { data } = await axios.post(
        `https://api.cloudinary.com/v1_1/${signatureData.cloudName}/auto/upload`,
        formData,
        {
          onUploadProgress: (e) => {
            const percent = Math.round((e.loaded * 100) / e.total);
            setProgress(percent);
          },
        }
      );

      setUploadedUrl(data.secure_url);
      inputRef.current.value = null;
    } catch (err) {
      console.error('Upload failed:', err);
      alert('Upload failed. Check console.');
    }
  };

  return (
    <section style={{ padding: '1rem' }}>
      <h2>Secure File Upload to Cloudinary</h2>

      <input
        ref={inputRef}
        type="file"
        accept="image/*,video/*,.pdf,.doc,.docx"
        onChange={handleFileChange}
      />

      {previewUrl && file?.type.startsWith('image') && (
        <img src={previewUrl} alt="Preview" width={200} style={{ marginTop: '1rem' }} />
      )}

      {previewUrl && file?.type.startsWith('video') && (
        <video width={300} controls style={{ marginTop: '1rem' }}>
          <source src={previewUrl} type={file.type} />
        </video>
      )}

      {!previewUrl && file && (
        <p style={{ marginTop: '1rem' }}>Selected File: {file.name}</p>
      )}

      <button onClick={handleUpload} disabled={!file} style={{ marginTop: '1rem' }}>
        Upload
      </button>

      {progress > 0 && <p>Progress: {progress}%</p>}

      {uploadedUrl && (
        <div style={{ marginTop: '1rem' }}>
          <p>Uploaded Successfully!</p>
          <a href={uploadedUrl} target="_blank" rel="noopener noreferrer">View File</a>
        </div>
      )}
    </section>
  );
};

export default FileUploader;


Step 5. Use Component in App
client/src/App.jsx

import FileUploader from './components/FileUploader';

function App() {
  return (
    <div style={{ maxWidth: '600px', margin: '0 auto', fontFamily: 'sans-serif' }}>
      <h1>Cloudinary File Uploader</h1>
      <FileUploader />
    </div>
  );
}

export default App;

Why Use Signed Uploads?

Cloudinary offers two ways to upload files:

  • Unsigned Uploads: Anyone with your upload preset can upload files. Not recommended for production because it's insecure.
  • Signed Uploads (used in this guide): The backend signs each upload request using your Cloudinary secret key, making it secure. This ensures:
    • Files are uploaded only by authenticated users (if you add auth)
    • Upload presets can't be abused
    • You have more control over what's uploaded

Best Practices

  • Use /auto/upload endpoint to auto-detect file type (image/video/raw)
  • Don’t expose Cloudinary secret API keys in frontend
  • Limit file size on client and/or backend

Supported File Types
Cloudinary accepts:

  • Images: jpg, png, webp, etc.
  • Videos: mp4, mov, avi
  • Documents: pdf, doc, docx, txt (uploaded as raw)

Conclusion
In this post, we developed a cutting-edge React file uploader that works flawlessly with Cloudinary. It offers a safe, production-ready starting point, preview capabilities, progress tracking, and support for a variety of file types. Blogs, admin panels, profile setups, and CMSs can all make use of this uploader. Take into account backend signed uploads or Cloudinary's transformation capabilities for more complex use cases.