Complete Guide to File Uploads in Next.js:EdgeStore, Cloudinary & UploadThing Implementation
Master file uploads in Next.js with three powerful services. Learn to implement single and multiple image/file uploads using EdgeStore's type-safe components, Cloudinary's transformation APIs, and UploadThing's developer-friendly widgets..
Complete Guide to File Uploads in Next.js: EdgeStore, Cloudinary & UploadThing Implementation
Complete File Upload Guide: EdgeStore, Cloudinary & UploadThing
This comprehensive guide will help you implement image and file uploads using three powerful services: EdgeStore, Next-Cloudinary, and UploadThing. Each service offers pre-built UI components and provides support for both single and multiple file uploads.
Table of Contents
- EdgeStore Implementation
- Next-Cloudinary Implementation
- UploadThing Implementation
- Universal Upload Component Examples
EdgeStore Implementation
EdgeStore provides type-safe, fast, scalable and secure storage solutions with beautiful pre-built components.
1. Installation & Setup
pnpm add @edgestore/server @edgestore/react zod
2. Environment Variables
Create a .env.local
file and add your EdgeStore credentials:
EDGE_STORE_ACCESS_KEY=your-access-key
EDGE_STORE_SECRET_KEY=your-secret-key
3. Backend Setup
Create app/api/edgestore/route.ts
(App Router):
import { initEdgeStore } from "@edgestore/server";
import { createEdgeStoreNextHandler } from "@edgestore/server/adapters/next/app";
const es = initEdgeStore.create();
const edgeStoreRouter = es.router({
publicFiles: es.fileBucket({
maxSize: 1024 * 1024 * 10, // 10MB
accept: ["image/*", "application/pdf", "text/*"], // Allow images, PDFs, text files
}),
publicImages: es.imageBucket({
maxSize: 1024 * 1024 * 5, // 5MB
accept: ["image/jpeg", "image/png", "image/webp"],
}),
});
const handler = createEdgeStoreNextHandler({
router: edgeStoreRouter,
});
export { handler as GET, handler as POST };
export type EdgeStoreRouter = typeof edgeStoreRouter;
4. Context Provider
Create lib/edgestore.ts
:
"use client";
import { type EdgeStoreRouter } from "../app/api/edgestore/route";
import { createEdgeStoreProvider } from "@edgestore/react";
const { EdgeStoreProvider, useEdgeStore } =
createEdgeStoreProvider<EdgeStoreRouter>({
maxConcurrentUploads: 5,
});
export { EdgeStoreProvider, useEdgeStore };
Update app/layout.tsx
:
import { EdgeStoreProvider } from '../lib/edgestore';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<EdgeStoreProvider>{children}</EdgeStoreProvider>
</body>
</html>
);
}
5. Install Pre-built Components
pnpm dlx shadcn@latest add https://edgestore.dev/r/single-image-dropzone.json
npx shadcn@latest add https://edgestore.dev/r/multi-image-dropzone.json
npx shadcn@latest add https://edgestore.dev/r/multi-file-dropzone.json
6. Universal EdgeStore Upload Component
Create components/EdgeStoreUploader.tsx
:
'use client';
import * as React from 'react';
import { useEdgeStore } from '@/lib/edgestore';
import { SingleImageDropzone } from '@/components/upload/single-image';
import { MultiImageDropzone } from '@/components/upload/multi-image';
import { FileUploader } from '@/components/upload/multi-file';
import { UploaderProvider, type UploadFn } from '@/components/upload/uploader-provider';
interface EdgeStoreUploaderProps {
type: 'image' | 'file';
multiple?: boolean;
maxFiles?: number;
maxSize?: number; // in bytes
onUploadComplete?: (urls: string[]) => void;
onUploadError?: (error: Error) => void;
}
export function EdgeStoreUploader({
type,
multiple = false,
maxFiles = 5,
maxSize = 1024 * 1024 * 5, // 5MB default
onUploadComplete,
onUploadError,
}: EdgeStoreUploaderProps) {
const { edgestore } = useEdgeStore();
const [uploadedFiles, setUploadedFiles] = React.useState<string[]>([]);
const uploadFn: UploadFn = React.useCallback(
async ({ file, onProgressChange, signal }) => {
try {
const bucket = type === 'image' ? edgestore.publicImages : edgestore.publicFiles;
const res = await bucket.upload({
file,
signal,
onProgressChange,
});
// Track uploaded files for potential deletion
setUploadedFiles(prev => [...prev, res.url]);
return res;
} catch (error) {
onUploadError?.(error as Error);
throw error;
}
},
[edgestore, type, onUploadError]
);
const handleUploadComplete = React.useCallback(
(results: any[]) => {
const urls = results.map(result => result.url);
onUploadComplete?.(urls);
},
[onUploadComplete]
);
// Delete function
const deleteFile = async (url: string) => {
try {
const bucket = type === 'image' ? edgestore.publicImages : edgestore.publicFiles;
await bucket.delete({ url });
setUploadedFiles(prev => prev.filter(fileUrl => fileUrl !== url));
} catch (error) {
console.error('Failed to delete file:', error);
}
};
return (
<div className="space-y-4">
<UploaderProvider uploadFn={uploadFn} autoUpload>
{type === 'image' && !multiple && (
<SingleImageDropzone
width={200}
height={200}
dropzoneOptions={{
maxSize,
}}
/>
)}
{type === 'image' && multiple && (
<MultiImageDropzone
maxFiles={maxFiles}
maxSize={maxSize}
onFilesAdded={handleUploadComplete}
/>
)}
{type === 'file' && (
<FileUploader
maxFiles={multiple ? maxFiles : 1}
maxSize={maxSize}
accept={{
'application/pdf': ['.pdf'],
'text/plain': ['.txt'],
'application/msword': ['.doc'],
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'],
}}
/>
)}
</UploaderProvider>
{/* Display uploaded files with delete buttons */}
{uploadedFiles.length > 0 && (
<div className="space-y-2">
<h3 className="font-medium">Uploaded Files:</h3>
{uploadedFiles.map((url, index) => (
<div key={index} className="flex items-center justify-between p-2 border rounded">
<a href={url} target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:underline truncate">
{url.split('/').pop()}
</a>
<button
onClick={() => deleteFile(url)}
className="px-2 py-1 text-xs bg-red-500 text-white rounded hover:bg-red-600"
>
Delete
</button>
</div>
))}
</div>
)}
</div>
);
}
7. Usage Examples
'use client';
import { EdgeStoreUploader } from '@/components/EdgeStoreUploader';
export default function UploadPage() {
return (
<div className="p-8 space-y-8">
{/* Single Image Upload */}
<div>
<h2>Single Image Upload</h2>
<EdgeStoreUploader
type="image"
multiple={false}
onUploadComplete={(urls) => console.log('Uploaded:', urls)}
/>
</div>
{/* Multiple Image Upload */}
<div>
<h2>Multiple Image Upload</h2>
<EdgeStoreUploader
type="image"
multiple={true}
maxFiles={3}
onUploadComplete={(urls) => console.log('Uploaded:', urls)}
/>
</div>
{/* Single File Upload */}
<div>
<h2>Single File Upload</h2>
<EdgeStoreUploader
type="file"
multiple={false}
maxSize={1024 * 1024 * 10} // 10MB
onUploadComplete={(urls) => console.log('Uploaded:', urls)}
/>
</div>
{/* Multiple File Upload */}
<div>
<h2>Multiple File Upload</h2>
<EdgeStoreUploader
type="file"
multiple={true}
maxFiles={5}
onUploadComplete={(urls) => console.log('Uploaded:', urls)}
/>
</div>
</div>
);
}
Next-Cloudinary Implementation
Next-Cloudinary provides powerful image and video APIs with pre-built upload widgets.
1. Installation & Setup
pnpm add next-cloudinary cloudinary
2. Environment Variables
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=your-cloud-name
NEXT_PUBLIC_CLOUDINARY_API_KEY=your-api-key
CLOUDINARY_API_SECRET=your-api-secret
NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET=your-upload-preset
3. Create Upload Preset
- Go to your Cloudinary dashboard
- Navigate to Settings → Upload
- Scroll to "Upload presets"
- Create a new preset with your desired settings
- Set the signing mode to "Unsigned" for client-side uploads
4. API Route for Signed Uploads (Optional but Recommended)
Create app/api/sign-cloudinary-params/route.ts
:
import { v2 as cloudinary } from "cloudinary";
cloudinary.config({
cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,
api_key: process.env.NEXT_PUBLIC_CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
});
export async function POST(request: Request) {
const body = await request.json();
const { paramsToSign } = body;
const signature = cloudinary.utils.api_sign_request(
paramsToSign,
process.env.CLOUDINARY_API_SECRET!
);
return Response.json({ signature });
}
5. Universal Cloudinary Upload Component
Create components/CloudinaryUploader.tsx
:
'use client';
import { useState } from 'react';
import { CldUploadWidget } from 'next-cloudinary';
interface CloudinaryUploaderProps {
type: 'image' | 'video' | 'auto';
multiple?: boolean;
maxFiles?: number;
onUploadComplete?: (results: any[]) => void;
onUploadError?: (error: Error) => void;
}
export function CloudinaryUploader({
type,
multiple = false,
maxFiles = 5,
onUploadComplete,
onUploadError,
}: CloudinaryUploaderProps) {
const [uploadedFiles, setUploadedFiles] = useState<any[]>([]);
const resourceType = type === 'auto' ? 'auto' : type;
const handleSuccess = (result: any) => {
const newFile = {
url: result.info.secure_url,
public_id: result.info.public_id,
resource_type: result.info.resource_type,
format: result.info.format,
};
setUploadedFiles(prev => {
const updated = multiple ? [...prev, newFile] : [newFile];
onUploadComplete?.(updated);
return updated;
});
};
const handleError = (error: any) => {
onUploadError?.(new Error(error.statusText || 'Upload failed'));
};
const deleteFile = async (publicId: string) => {
try {
// Note: Deletion through client-side requires admin API or server action
// This is a placeholder - implement server action for deletion
await fetch('/api/delete-cloudinary-asset', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ public_id: publicId }),
});
setUploadedFiles(prev => prev.filter(file => file.public_id !== publicId));
} catch (error) {
console.error('Failed to delete file:', error);
}
};
return (
<div className="space-y-4">
<CldUploadWidget
uploadPreset={process.env.NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET}
signatureEndpoint="/api/sign-cloudinary-params"
onSuccess={handleSuccess}
onError={handleError}
options={{
multiple,
maxFiles: multiple ? maxFiles : 1,
resourceType,
sources: ['local', 'url', 'camera'],
showAdvancedOptions: false,
showInsecurePreview: true,
showCompletedButton: true,
showUploadMoreButton: multiple,
folder: `uploads/${type}s`, // Organize by type
}}
>
{({ open }) => (
<button
onClick={() => open()}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
>
Upload {multiple ? `${type}s` : type.charAt(0).toUpperCase() + type.slice(1)}
</button>
)}
</CldUploadWidget>
{/* Display uploaded files */}
{uploadedFiles.length > 0 && (
<div className="space-y-2">
<h3 className="font-medium">Uploaded Files:</h3>
<div className="grid gap-4">
{uploadedFiles.map((file, index) => (
<div key={index} className="flex items-center justify-between p-4 border rounded">
<div className="flex items-center space-x-4">
{file.resource_type === 'image' ? (
<img
src={file.url}
alt="Uploaded"
className="w-16 h-16 object-cover rounded"
/>
) : (
<div className="w-16 h-16 bg-gray-200 rounded flex items-center justify-center">
📄
</div>
)}
<div>
<p className="font-medium">{file.public_id}</p>
<p className="text-sm text-gray-500">{file.format?.toUpperCase()}</p>
</div>
</div>
<button
onClick={() => deleteFile(file.public_id)}
className="px-3 py-1 text-sm bg-red-500 text-white rounded hover:bg-red-600"
>
Delete
</button>
</div>
))}
</div>
</div>
)}
</div>
);
}
6. Delete API Route (Optional)
Create app/api/delete-cloudinary-asset/route.ts
:
import { v2 as cloudinary } from "cloudinary";
cloudinary.config({
cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,
api_key: process.env.NEXT_PUBLIC_CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
});
export async function POST(request: Request) {
try {
const { public_id } = await request.json();
const result = await cloudinary.uploader.destroy(public_id);
return Response.json({ success: true, result });
} catch (error) {
return Response.json(
{ success: false, error: "Failed to delete asset" },
{ status: 500 }
);
}
}
7. Usage Examples
'use client';
import { CloudinaryUploader } from '@/components/CloudinaryUploader';
export default function CloudinaryPage() {
return (
<div className="p-8 space-y-8">
{/* Single Image Upload */}
<div>
<h2>Single Image Upload</h2>
<CloudinaryUploader
type="image"
multiple={false}
onUploadComplete={(results) => console.log('Images uploaded:', results)}
/>
</div>
{/* Multiple Image Upload */}
<div>
<h2>Multiple Image Upload</h2>
<CloudinaryUploader
type="image"
multiple={true}
maxFiles={5}
onUploadComplete={(results) => console.log('Images uploaded:', results)}
/>
</div>
{/* Video Upload */}
<div>
<h2>Video Upload</h2>
<CloudinaryUploader
type="video"
onUploadComplete={(results) => console.log('Video uploaded:', results)}
/>
</div>
{/* Auto Upload (Any File Type) */}
<div>
<h2>Any File Upload</h2>
<CloudinaryUploader
type="auto"
multiple={true}
maxFiles={3}
onUploadComplete={(results) => console.log('Files uploaded:', results)}
/>
</div>
</div>
);
}
UploadThing Implementation
UploadThing is designed specifically for full-stack TypeScript applications with excellent developer experience.
1. Installation & Setup
pnpm add uploadthing @uploadthing/react
2. Environment Variables
UPLOADTHING_SECRET=your-secret-key
UPLOADTHING_APP_ID=your-app-id
3. File Router Setup
Create app/api/uploadthing/core.ts
:
import { createUploadthing, type FileRouter } from "uploadthing/next";
import { z } from "zod";
const f = createUploadthing();
export const ourFileRouter = {
// Image uploader
imageUploader: f({ image: { maxFileSize: "4MB", maxFileCount: 10 } })
.middleware(async ({ req }) => {
// Authentication logic here if needed
// const user = await auth(req);
// if (!user) throw new UploadThingError("Unauthorized");
return { uploadedBy: "user" }; // Metadata
})
.onUploadComplete(async ({ metadata, file }) => {
console.log("Upload complete for user:", metadata.uploadedBy);
console.log("File URL:", file.url);
return { uploadedBy: metadata.uploadedBy };
}),
// PDF uploader
pdfUploader: f({ pdf: { maxFileSize: "16MB", maxFileCount: 5 } })
.middleware(async ({ req }) => {
return { uploadedBy: "user" };
})
.onUploadComplete(async ({ metadata, file }) => {
console.log("PDF upload complete:", file.url);
return { uploadedBy: metadata.uploadedBy };
}),
// General file uploader
fileUploader: f(["image", "video", "audio", "pdf", "text"])
.middleware(async ({ req }) => {
return { uploadedBy: "user" };
})
.onUploadComplete(async ({ metadata, file }) => {
console.log("File upload complete:", file.url);
return { uploadedBy: metadata.uploadedBy };
}),
} satisfies FileRouter;
export type OurFileRouter = typeof ourFileRouter;
4. API Route Handler
Create app/api/uploadthing/route.ts
:
import { createRouteHandler } from "uploadthing/next";
import { ourFileRouter } from "./core";
export const { GET, POST } = createRouteHandler({
router: ourFileRouter,
});
5. Generate Components
Create utils/uploadthing.ts
:
import {
generateUploadButton,
generateUploadDropzone,
} from "@uploadthing/react";
import type { OurFileRouter } from "@/app/api/uploadthing/core";
export const UploadButton = generateUploadButton<OurFileRouter>();
export const UploadDropzone = generateUploadDropzone<OurFileRouter>();
6. Universal UploadThing Component
Create components/UploadThingUploader.tsx
:
'use client';
import { useState } from "react";
import { UploadButton, UploadDropzone } from "@/utils/uploadthing";
import { UTApi } from "uploadthing/server";
interface UploadThingUploaderProps {
endpoint: 'imageUploader' | 'pdfUploader' | 'fileUploader';
multiple?: boolean;
variant?: 'button' | 'dropzone';
onUploadComplete?: (files: any[]) => void;
onUploadError?: (error: Error) => void;
}
export function UploadThingUploader({
endpoint,
multiple = false,
variant = 'button',
onUploadComplete,
onUploadError,
}: UploadThingUploaderProps) {
const [uploadedFiles, setUploadedFiles] = useState<any[]>([]);
const handleUploadComplete = (res: any[]) => {
setUploadedFiles(prev => multiple ? [...prev, ...res] : res);
onUploadComplete?.(res);
};
const handleUploadError = (error: Error) => {
onUploadError?.(error);
};
const deleteFile = async (fileKey: string) => {
try {
// Create server action or API route for deletion
await fetch('/api/delete-uploadthing-file', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fileKey }),
});
setUploadedFiles(prev => prev.filter(file => file.key !== fileKey));
} catch (error) {
console.error('Failed to delete file:', error);
}
};
const commonProps = {
endpoint,
onClientUploadComplete: handleUploadComplete,
onUploadError: handleUploadError,
};
return (
<div className="space-y-4">
{variant === 'button' ? (
<UploadButton
{...commonProps}
appearance={{
button: "ut-ready:bg-green-500 ut-uploading:cursor-not-allowed rounded-r-none bg-red-500 bg-none after:bg-orange-400",
allowedContent: "flex h-8 flex-col items-center justify-center px-2 text-white",
}}
/>
) : (
<UploadDropzone
{...commonProps}
appearance={{
container: "w-full max-w-md border-2 border-dashed border-gray-300 rounded-lg p-6",
uploadIcon: "text-gray-400",
label: "text-gray-600",
allowedContent: "text-gray-500 text-sm",
}}
/>
)}
{/* Display uploaded files */}
{uploadedFiles.length > 0 && (
<div className="space-y-2">
<h3 className="font-medium">Uploaded Files:</h3>
<div className="space-y-2">
{uploadedFiles.map((file, index) => (
<div key={index} className="flex items-center justify-between p-3 border rounded">
<div className="flex items-center space-x-3">
{file.type?.startsWith('image/') ? (
<img
src={file.url}
alt={file.name}
className="w-12 h-12 object-cover rounded"
/>
) : (
<div className="w-12 h-12 bg-gray-200 rounded flex items-center justify-center">
📄
</div>
)}
<div>
<p className="font-medium text-sm">{file.name}</p>
<p className="text-xs text-gray-500">
{(file.size / 1024).toFixed(1)} KB
</p>
</div>
</div>
<button
onClick={() => deleteFile(file.key)}
className="px-3 py-1 text-sm bg-red-500 text-white rounded hover:bg-red-600"
>
Delete
</button>
</div>
))}
</div>
</div>
)}
</div>
);
}
7. Delete API Route
Create app/api/delete-uploadthing-file/route.ts
:
import { UTApi } from "uploadthing/server";
const utapi = new UTApi();
export async function POST(request: Request) {
try {
const { fileKey } = await request.json();
await utapi.deleteFiles([fileKey]);
return Response.json({ success: true });
} catch (error) {
console.error("Delete error:", error);
return Response.json(
{ success: false, error: "Failed to delete file" },
{ status: 500 }
);
}
}
8. Usage Examples
'use client';
import { UploadThingUploader } from '@/components/UploadThingUploader';
export default function UploadThingPage() {
return (
<div className="p-8 space-y-8">
{/* Single Image Upload - Button */}
<div>
<h2>Single Image Upload (Button)</h2>
<UploadThingUploader
endpoint="imageUploader"
multiple={false}
variant="button"
onUploadComplete={(files) => console.log('Images uploaded:', files)}
/>
</div>
{/* Multiple Image Upload - Dropzone */}
<div>
<h2>Multiple Image Upload (Dropzone)</h2>
<UploadThingUploader
endpoint="imageUploader"
multiple={true}
variant="dropzone"
onUploadComplete={(files) => console.log('Images uploaded:', files)}
/>
</div>
{/* PDF Upload */}
<div>
<h2>PDF Upload</h2>
<UploadThingUploader
endpoint="pdfUploader"
variant="dropzone"
onUploadComplete={(files) => console.log('PDFs uploaded:', files)}
/>
</div>
{/* General File Upload */}
<div>
<h2>General File Upload</h2>
<UploadThingUploader
endpoint="fileUploader"
multiple={true}
variant="dropzone"
onUploadComplete={(files) => console.log('Files uploaded:', files)}
/>
</div>
</div>
);
}
Universal Upload Component Examples
Here are examples of how to create components that can switch between different upload services based on configuration:
Multi-Service Upload Component
'use client';
import { EdgeStoreUploader } from '@/components/EdgeStoreUploader';
import { CloudinaryUploader } from '@/components/CloudinaryUploader';
import { UploadThingUploader } from '@/components/UploadThingUploader';
interface UniversalUploaderProps {
service: 'edgestore' | 'cloudinary' | 'uploadthing';
type: 'image' | 'file' | 'video' | 'auto';
multiple?: boolean;
maxFiles?: number;
onUploadComplete?: (results: any[]) => void;
onUploadError?: (error: Error) => void;
}
export function UniversalUploader({
service,
type,
multiple = false,
maxFiles = 5,
onUploadComplete,
onUploadError,
}: UniversalUploaderProps) {
switch (service) {
case 'edgestore':
return (
<EdgeStoreUploader
type={type as 'image' | 'file'}
multiple={multiple}
maxFiles={maxFiles}
onUploadComplete={onUploadComplete}
onUploadError={onUploadError}
/>
);
case 'cloudinary':
return (
<CloudinaryUploader
type={type as 'image' | 'video' | 'auto'}
multiple={multiple}
maxFiles={maxFiles}
onUploadComplete={onUploadComplete}
onUploadError={onUploadError}
/>
);
case 'uploadthing':
const endpoint = type === 'image' ? 'imageUploader' :
type === 'file' ? 'fileUploader' : 'fileUploader';
return (
<UploadThingUploader
endpoint={endpoint as 'imageUploader' | 'pdfUploader' | 'fileUploader'}
multiple={multiple}
variant="dropzone"
onUploadComplete={onUploadComplete}
onUploadError={onUploadError}
/>
);
default:
return <div>Unsupported upload service</div>;
}
}
Usage of Universal Component
'use client';
import { UniversalUploader } from '@/components/UniversalUploader';
import { useState } from 'react';
export default function UniversalUploadPage() {
const [selectedService, setSelectedService] = useState<'edgestore' | 'cloudinary' | 'uploadthing'>('edgestore');
const [uploadType, setUploadType] = useState<'image' | 'file'>('image');
const [multiple, setMultiple] = useState(false);
return (
<div className="p-8 max-w-4xl mx-auto">
<h1 className="text-3xl font-bold mb-8">Universal File Upload</h1>
{/* Service Selection */}
<div className="mb-6 p-4 border rounded">
<h3 className="font-medium mb-3">Select Upload Service:</h3>
<div className="flex space-x-4">
{(['edgestore', 'cloudinary', 'uploadthing'] as const).map((service) => (
<label key={service} className="flex items-center">
<input
type="radio"
value={service}
checked={selectedService === service}
onChange={(e) => setSelectedService(e.target.value as any)}
className="mr-2"
/>
{service.charAt(0).toUpperCase() + service.slice(1)}
</label>
))}
</div>
</div>
{/* Type Selection */}
<div className="mb-6 p-4 border rounded">
<h3 className="font-medium mb-3">Select Upload Type:</h3>
<div className="flex space-x-4">
<label className="flex items-center">
<input
type="radio"
value="image"
checked={uploadType === 'image'}
onChange={(e) => setUploadType(e.target.value as any)}
className="mr-2"
/>
Images
</label>
<label className="flex items-center">
<input
type="radio"
value="file"
checked={uploadType === 'file'}
onChange={(e) => setUploadType(e.target.value as any)}
className="mr-2"
/>
Files
</label>
</div>
</div>
{/* Multiple Selection */}
<div className="mb-6 p-4 border rounded">
<label className="flex items-center">
<input
type="checkbox"
checked={multiple}
onChange={(e) => setMultiple(e.target.checked)}
className="mr-2"
/>
Allow multiple uploads
</label>
</div>
{/* Upload Component */}
<div className="border-2 border-dashed border-gray-300 rounded-lg p-8">
<UniversalUploader
service={selectedService}
type={uploadType}
multiple={multiple}
maxFiles={5}
onUploadComplete={(results) => {
console.log(`${selectedService} upload complete:`, results);
}}
onUploadError={(error) => {
console.error(`${selectedService} upload error:`, error);
}}
/>
</div>
</div>
);
}
Step-by-Step Implementation Summary
EdgeStore Steps:
- Install packages:
@edgestore/server @edgestore/react zod
- Setup environment variables:
EDGE_STORE_ACCESS_KEY
andEDGE_STORE_SECRET_KEY
- Create API route:
app/api/edgestore/route.ts
with file and image buckets - Setup context provider: Wrap app with
EdgeStoreProvider
- Install pre-built components: Use shadcn CLI commands
- Create universal component: Combine all EdgeStore components with props
- Implement delete functionality: Use EdgeStore's delete method
- Usage: Pass
type
,multiple
, and callbacks as props
Cloudinary Steps:
- Install packages:
next-cloudinary cloudinary
- Setup environment variables: Cloud name, API key, secret, and upload preset
- Create upload preset: In Cloudinary dashboard (unsigned mode)
- Create signing endpoint:
app/api/sign-cloudinary-params/route.ts
for security - Create universal component: Use
CldUploadWidget
with different configurations - Implement delete functionality: Create API route using Cloudinary's destroy method
- Usage: Configure upload widget options based on file type and multiple settings
UploadThing Steps:
- Install packages:
uploadthing @uploadthing/react
- Setup environment variables:
UPLOADTHING_SECRET
andUPLOADTHING_APP_ID
- Create file router:
app/api/uploadthing/core.ts
with different endpoints - Create API handler:
app/api/uploadthing/route.ts
- Generate components: Create typed upload components
- Create universal component: Switch between button and dropzone variants
- Implement delete functionality: Use UTApi for server-side deletion
- Usage: Select appropriate endpoint based on file type
Key Features Comparison
Feature | EdgeStore | Cloudinary | UploadThing |
---|---|---|---|
File Types | Images, Files | Images, Videos, Files | Images, Videos, PDFs, Files |
Components | Pre-built dropzones | Upload widget | Button & Dropzone |
TypeScript | ✅ Full support | ✅ Good support | ✅ Excellent support |
Multiple Uploads | ✅ Built-in | ✅ Widget option | ✅ Component support |
Progress Tracking | ✅ Built-in | ✅ Widget events | ✅ Built-in |
File Deletion | ✅ Client & Server | ✅ Server-side | ✅ Server-side |
Image Processing | ✅ Automatic thumbnails | ✅ Advanced transformations | ❌ Basic |
Free Tier | 1.5GB | 25GB + transformations | 2GB |
CDN | ✅ Built-in | ✅ Global CDN | ✅ Built-in |
Security | ✅ Lifecycle hooks | ✅ Signed uploads | ✅ Middleware auth |
Best Practices
Security
- Always use signed uploads in production
- Implement proper authentication in middleware/lifecycle hooks
- Validate file types and sizes on both client and server
- Use environment variables for API keys
Performance
- Implement progress indicators for better UX
- Use appropriate file size limits
- Consider lazy loading for file previews
- Implement proper error handling
User Experience
- Provide clear upload instructions
- Show upload progress and status
- Allow drag & drop functionality
- Implement proper loading states
File Management
- Organize files in folders/categories
- Implement proper cleanup for temporary files
- Consider file versioning for updates
- Monitor storage usage and costs
This comprehensive guide provides everything you need to implement file uploads with EdgeStore, Cloudinary, and UploadThing. Each service has its strengths:
- EdgeStore: Great for TypeScript projects with beautiful pre-built components
- Cloudinary: Best for image/video processing with powerful transformation APIs
- UploadThing: Excellent developer experience with full-stack TypeScript integration
Choose based on your specific needs, budget, and technical requirements!