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