JB logo

Command Palette

Search for a command to run...

yOUTUBE
Blog
Next

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

  1. EdgeStore Implementation
  2. Next-Cloudinary Implementation
  3. UploadThing Implementation
  4. 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

  1. Go to your Cloudinary dashboard
  2. Navigate to Settings → Upload
  3. Scroll to "Upload presets"
  4. Create a new preset with your desired settings
  5. Set the signing mode to "Unsigned" for client-side uploads

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:

  1. Install packages: @edgestore/server @edgestore/react zod
  2. Setup environment variables: EDGE_STORE_ACCESS_KEY and EDGE_STORE_SECRET_KEY
  3. Create API route: app/api/edgestore/route.ts with file and image buckets
  4. Setup context provider: Wrap app with EdgeStoreProvider
  5. Install pre-built components: Use shadcn CLI commands
  6. Create universal component: Combine all EdgeStore components with props
  7. Implement delete functionality: Use EdgeStore's delete method
  8. Usage: Pass type, multiple, and callbacks as props

Cloudinary Steps:

  1. Install packages: next-cloudinary cloudinary
  2. Setup environment variables: Cloud name, API key, secret, and upload preset
  3. Create upload preset: In Cloudinary dashboard (unsigned mode)
  4. Create signing endpoint: app/api/sign-cloudinary-params/route.ts for security
  5. Create universal component: Use CldUploadWidget with different configurations
  6. Implement delete functionality: Create API route using Cloudinary's destroy method
  7. Usage: Configure upload widget options based on file type and multiple settings

UploadThing Steps:

  1. Install packages: uploadthing @uploadthing/react
  2. Setup environment variables: UPLOADTHING_SECRET and UPLOADTHING_APP_ID
  3. Create file router: app/api/uploadthing/core.ts with different endpoints
  4. Create API handler: app/api/uploadthing/route.ts
  5. Generate components: Create typed upload components
  6. Create universal component: Switch between button and dropzone variants
  7. Implement delete functionality: Use UTApi for server-side deletion
  8. Usage: Select appropriate endpoint based on file type

Key Features Comparison

FeatureEdgeStoreCloudinaryUploadThing
File TypesImages, FilesImages, Videos, FilesImages, Videos, PDFs, Files
ComponentsPre-built dropzonesUpload widgetButton & 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 Tier1.5GB25GB + transformations2GB
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!