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
-
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/uploadendpoint 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.

Join the conversation! Your thoughts help the community grow.