Node.js  

Upload Files from React to Cloudinary: Full Guide with Preview, Progress & Secure Backend (2025)

Introduction

Uploading files like images, videos, and documents is a core feature in many modern web applications. In this guide, we'll walk through how to implement a robust file upload feature in a React (v18+) application using Cloudinary, a powerful media management platform. We'll also cover secure backend-signed uploads using Node.js (Express).

We'll build a reusable upload component that supports multiple file types, shows a preview (where possible), tracks upload progress, and securely uploads media using a backend-generated signature. We'll use functional components, React Hooks, and modern best practices.

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

  1. Sign up at cloudinary.com

  2. Go to your dashboard and note:

    • Cloud Name

    • API Key

    • API Secret

  3. 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 article, we built a modern React file uploader that integrates seamlessly with Cloudinary. It supports multiple file types, preview functionality, progress tracking, and provides a secure, production-ready starting point.

This uploader can be reused in blogs, admin panels, profile setups, or CMSs. For more advanced use cases, consider Cloudinary's transformation features or backend signed uploads.