More File Upload and Management Libraries
Exploring popular file upload libraries and services for different use cases and requirements.
File Upload Libraries Landscape
While implementing file uploads from scratch gives you full control, using established libraries can save significant development time and provide better user experiences. This lesson explores popular file upload solutions for Next.js applications.
Popular Libraries Covered
This lesson explores the following file upload libraries:
- UploadThing - Modern Next.js-focused upload service with type safety
- React Dropzone - Flexible drag-and-drop library with full customization
- Uppy - Modular uploader with multiple sources and resumable uploads
- FilePond - Beautiful UI with built-in image transformations
- Cloudinary - Complete media management platform with CDN
- AWS S3 Direct Upload - Enterprise-grade scalable storage solution
UploadThing
UploadThing is a modern file upload service built specifically for the Next.js ecosystem. It provides a type-safe, serverless-friendly solution with minimal configuration.
Key Features
- Type-Safe - Full TypeScript support with automatic type inference
- Serverless-Friendly - Works perfectly with Vercel and other serverless platforms
- Built-in Dashboard - File management UI included
- Next.js Integration - First-class Next.js support
- Free Tier - Generous free plan for getting started
- Presigned URLs - Secure direct-to-storage uploads
- Automatic Optimization - Image processing included
Installation
yarn add uploadthing @uploadthing/react
Setup
1. Get API Keys:
Sign up at uploadthing.com and get your API keys.
2. Environment Variables:
# .env.local
UPLOADTHING_SECRET=sk_live_...
UPLOADTHING_APP_ID=your_app_id
3. Create Upload Router:
// app/api/uploadthing/core.ts
import { createUploadthing, type FileRouter } from "uploadthing/next"
const f = createUploadthing()
export const ourFileRouter = {
imageUploader: f({ image: { maxFileSize: "4MB" } })
.middleware(async ({ req }) => {
// Authentication
const user = await auth(req)
if (!user) throw new Error("Unauthorized")
return { userId: user.id }
})
.onUploadComplete(async ({ metadata, file }) => {
console.log("Upload complete for userId:", metadata.userId)
console.log("file url", file.url)
// Save to database
await db.file.create({
data: {
url: file.url,
userId: metadata.userId,
}
})
return { uploadedBy: metadata.userId }
}),
pdfUploader: f({ pdf: { maxFileSize: "16MB" } })
.middleware(async ({ req }) => {
const user = await auth(req)
if (!user) throw new Error("Unauthorized")
return { userId: user.id }
})
.onUploadComplete(async ({ metadata, file }) => {
console.log("PDF uploaded:", file.url)
}),
} satisfies FileRouter
export type OurFileRouter = typeof ourFileRouter
4. Create API Route:
// app/api/uploadthing/route.ts
import { createRouteHandler } from "uploadthing/next"
import { ourFileRouter } from "./core"
export const { GET, POST } = createRouteHandler({
router: ourFileRouter,
})
5. Configure UploadThing:
// lib/uploadthing.ts
import { generateReactHelpers } from "@uploadthing/react"
import type { OurFileRouter } from "@/app/api/uploadthing/core"
export const { useUploadThing, uploadFiles } =
generateReactHelpers<OurFileRouter>()
Basic Usage
"use client"
import { UploadButton, UploadDropzone } from "@uploadthing/react"
import type { OurFileRouter } from "@/app/api/uploadthing/core"
export default function UploadExample() {
return (
<div>
<UploadButton<OurFileRouter>
endpoint="imageUploader"
onClientUploadComplete={(res) => {
console.log("Files: ", res)
alert("Upload Completed")
}}
onUploadError={(error: Error) => {
alert(`ERROR! ${error.message}`)
}}
/>
<UploadDropzone<OurFileRouter>
endpoint="imageUploader"
onClientUploadComplete={(res) => {
console.log("Files: ", res)
}}
onUploadError={(error: Error) => {
alert(`ERROR! ${error.message}`)
}}
/>
</div>
)
}
Custom Component
"use client"
import { useUploadThing } from "@/lib/uploadthing"
import { useState } from "react"
export default function CustomUploader() {
const [files, setFiles] = useState<File[]>([])
const { startUpload, isUploading } = useUploadThing("imageUploader", {
onClientUploadComplete: () => {
alert("uploaded successfully!")
},
onUploadError: () => {
alert("error occurred while uploading")
},
})
return (
<div>
<input
type="file"
multiple
onChange={(e) => {
const files = Array.from(e.target.files || [])
setFiles(files)
}}
/>
<button
onClick={() => startUpload(files)}
disabled={!files.length || isUploading}
>
{isUploading ? "Uploading..." : "Upload"}
</button>
</div>
)
}
Pros and Cons
Pros:
- Excellent Next.js integration
- Type-safe API
- Generous free tier
- Minimal setup required
- Built-in file management
- Automatic image optimization
Cons:
- Vendor lock-in
- Limited customization of storage
- Newer service (less battle-tested)
- Pricing can scale with usage
Best For
- Next.js applications
- Rapid prototyping
- Serverless deployments
- Projects needing quick file upload setup
- Teams wanting managed solution
React Dropzone
React Dropzone is a flexible, framework-agnostic library that provides drag-and-drop file upload functionality with extensive customization options.
Key Features
- Drag and Drop - Intuitive drag-and-drop interface
- File Validation - Built-in type and size validation
- Highly Customizable - Complete control over UI
- Accessibility - Keyboard navigation support
- Zero Dependencies - Lightweight library
- Framework Agnostic - Works anywhere React works
Installation
yarn add react-dropzone
Basic Usage
"use client"
import { useCallback } from 'react'
import { useDropzone } from 'react-dropzone'
export default function MyDropzone() {
const onDrop = useCallback((acceptedFiles: File[]) => {
// Handle files
console.log(acceptedFiles)
}, [])
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop
})
return (
<div
{...getRootProps()}
className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors ${
isDragActive ? 'border-blue-500 bg-blue-50' : 'border-gray-300'
}`}
>
<input {...getInputProps()} />
{isDragActive ? (
<p>Drop the files here...</p>
) : (
<p>Drag and drop files here, or click to select files</p>
)}
</div>
)
}
With File Validation
"use client"
import { useDropzone } from 'react-dropzone'
import { useState } from 'react'
export default function ValidatedDropzone() {
const [errors, setErrors] = useState<string[]>([])
const { getRootProps, getInputProps, fileRejections } = useDropzone({
accept: {
'image/*': ['.png', '.jpg', '.jpeg', '.gif']
},
maxSize: 5 * 1024 * 1024, // 5MB
maxFiles: 3,
onDropRejected: (fileRejections) => {
const messages = fileRejections.map(({ file, errors }) =>
`${file.name}: ${errors.map(e => e.message).join(', ')}`
)
setErrors(messages)
},
onDropAccepted: () => {
setErrors([])
}
})
return (
<div>
<div {...getRootProps()} className="border-2 border-dashed p-8">
<input {...getInputProps()} />
<p>Drop images here (max 5MB, up to 3 files)</p>
</div>
{errors.length > 0 && (
<div className="mt-4 text-red-600">
{errors.map((error, i) => (
<p key={i}>{error}</p>
))}
</div>
)}
</div>
)
}
With Preview
"use client"
import { useDropzone } from 'react-dropzone'
import { useState, useEffect } from 'react'
interface PreviewFile extends File {
preview: string
}
export default function DropzoneWithPreview() {
const [files, setFiles] = useState<PreviewFile[]>([])
const { getRootProps, getInputProps } = useDropzone({
accept: { 'image/*': [] },
onDrop: (acceptedFiles) => {
setFiles(
acceptedFiles.map((file) =>
Object.assign(file, {
preview: URL.createObjectURL(file)
})
)
)
}
})
useEffect(() => {
// Revoke object URLs to avoid memory leaks
return () => {
files.forEach(file => URL.revokeObjectURL(file.preview))
}
}, [files])
const thumbs = files.map((file) => (
<div key={file.name} className="inline-block w-32 h-32 p-1">
<img
src={file.preview}
alt={file.name}
className="w-full h-full object-cover rounded"
onLoad={() => URL.revokeObjectURL(file.preview)}
/>
</div>
))
return (
<div>
<div {...getRootProps()} className="border-2 border-dashed p-8">
<input {...getInputProps()} />
<p>Drag and drop images here</p>
</div>
<div className="mt-4 flex flex-wrap">{thumbs}</div>
</div>
)
}
Complete Upload Example
"use client"
import { useDropzone } from 'react-dropzone'
import { useState } from 'react'
export default function CompleteUploader() {
const [uploading, setUploading] = useState(false)
const [progress, setProgress] = useState(0)
const [uploadedUrl, setUploadedUrl] = useState<string>()
const { getRootProps, getInputProps, acceptedFiles } = useDropzone({
accept: { 'image/*': [] },
maxFiles: 1,
})
const handleUpload = async () => {
if (!acceptedFiles[0]) return
setUploading(true)
setProgress(0)
const formData = new FormData()
formData.append('file', acceptedFiles[0])
try {
const xhr = new XMLHttpRequest()
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percent = (e.loaded / e.total) * 100
setProgress(percent)
}
})
xhr.addEventListener('load', () => {
const response = JSON.parse(xhr.responseText)
setUploadedUrl(response.url)
setUploading(false)
})
xhr.open('POST', '/api/upload')
xhr.send(formData)
} catch (error) {
console.error('Upload failed:', error)
setUploading(false)
}
}
return (
<div className="space-y-4">
<div {...getRootProps()} className="border-2 border-dashed p-8">
<input {...getInputProps()} />
<p>Drag and drop an image, or click to select</p>
</div>
{acceptedFiles.length > 0 && (
<div>
<p>Selected: {acceptedFiles[0].name}</p>
<button
onClick={handleUpload}
disabled={uploading}
className="px-4 py-2 bg-blue-500 text-white rounded disabled:bg-gray-400"
>
{uploading ? 'Uploading...' : 'Upload'}
</button>
</div>
)}
{uploading && (
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-500 h-2 rounded-full transition-all"
style={{ width: `${progress}%` }}
/>
</div>
)}
{uploadedUrl && (
<div>
<p className="text-green-600">Upload successful!</p>
<img src={uploadedUrl} alt="Uploaded" className="max-w-xs mt-2" />
</div>
)}
</div>
)
}
Pros and Cons
Pros:
- Highly customizable
- Excellent drag-and-drop UX
- Built-in validation
- Lightweight
- Active maintenance
- Framework agnostic
Cons:
- No built-in upload logic
- No storage solution
- Requires backend implementation
- Manual progress tracking
Best For
- Projects needing custom file upload UI
- Applications requiring specific validation rules
- Teams wanting full control over storage
- Projects with existing backend infrastructure
Uppy
Uppy is a modular file uploader with support for multiple sources (local files, URLs, webcam, cloud storage) and advanced features like resumable uploads.
Key Features
- Modular - Use only the features you need
- Multiple Sources - Local, URL, Dropbox, Google Drive, Instagram
- Resumable Uploads - Continue interrupted uploads
- Progress Tracking - Detailed upload progress
- Image Editing - Built-in image cropping
- Webcam Support - Capture photos/videos directly
- Localization - Multi-language support
Installation
yarn add @uppy/core @uppy/react @uppy/dashboard @uppy/xhr-upload
Basic Usage
"use client"
import Uppy from '@uppy/core'
import { Dashboard } from '@uppy/react'
import XHRUpload from '@uppy/xhr-upload'
import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css'
export default function UppyUploader() {
const uppy = new Uppy({
restrictions: {
maxNumberOfFiles: 5,
maxFileSize: 10 * 1024 * 1024, // 10MB
allowedFileTypes: ['image/*', 'video/*']
}
}).use(XHRUpload, {
endpoint: '/api/upload',
fieldName: 'file',
})
uppy.on('complete', (result) => {
console.log('Upload complete:', result.successful)
})
return (
<Dashboard
uppy={uppy}
plugins={['Webcam']}
proudlyDisplayPoweredByUppy={false}
/>
)
}
With Image Editor
yarn add @uppy/image-editor
"use client"
import Uppy from '@uppy/core'
import { Dashboard } from '@uppy/react'
import ImageEditor from '@uppy/image-editor'
import XHRUpload from '@uppy/xhr-upload'
import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css'
import '@uppy/image-editor/dist/style.css'
export default function UppyWithEditor() {
const uppy = new Uppy({
restrictions: {
maxNumberOfFiles: 1,
allowedFileTypes: ['image/*']
}
})
.use(ImageEditor, {
quality: 0.8
})
.use(XHRUpload, {
endpoint: '/api/upload'
})
return (
<Dashboard
uppy={uppy}
plugins={['ImageEditor']}
height={550}
/>
)
}
With Cloud Sources
yarn add @uppy/google-drive @uppy/dropbox @uppy/url
"use client"
import Uppy from '@uppy/core'
import { Dashboard } from '@uppy/react'
import GoogleDrive from '@uppy/google-drive'
import Dropbox from '@uppy/dropbox'
import Url from '@uppy/url'
const uppy = new Uppy()
.use(GoogleDrive, {
companionUrl: 'https://companion.uppy.io'
})
.use(Dropbox, {
companionUrl: 'https://companion.uppy.io'
})
.use(Url, {
companionUrl: 'https://companion.uppy.io'
})
export default function UppyWithSources() {
return (
<Dashboard
uppy={uppy}
plugins={['GoogleDrive', 'Dropbox', 'Url']}
/>
)
}
Resumable Uploads (Tus)
yarn add @uppy/tus
import Uppy from '@uppy/core'
import Tus from '@uppy/tus'
import { Dashboard } from '@uppy/react'
const uppy = new Uppy()
.use(Tus, {
endpoint: 'https://tusd.tusdemo.net/files/',
resume: true,
autoRetry: true,
retryDelays: [0, 1000, 3000, 5000]
})
export default function ResumableUploader() {
return <Dashboard uppy={uppy} />
}
Pros and Cons
Pros:
- Feature-rich
- Modular architecture
- Multiple upload sources
- Resumable uploads
- Beautiful default UI
- Active development
Cons:
- Larger bundle size
- Complex setup for advanced features
- May need Companion server for cloud sources
- Steeper learning curve
Best For
- Applications needing multiple upload sources
- Large file uploads requiring resumability
- Projects wanting built-in image editing
- Applications with diverse file types
FilePond
FilePond is a JavaScript file upload library with a focus on providing a beautiful, user-friendly interface with minimal configuration.
Key Features
- Beautiful UI - Modern, polished interface
- Image Preview - Automatic image previews
- Progress Indicators - Visual upload progress
- File Validation - Built-in validation
- Image Transform - Resize, crop, optimize
- Drag and Drop - Intuitive interaction
- Async Uploads - Non-blocking uploads
Installation
yarn add react-filepond filepond
Basic Usage
"use client"
import { useState } from 'react'
import { FilePond, registerPlugin } from 'react-filepond'
import 'filepond/dist/filepond.min.css'
export default function FilePondUploader() {
const [files, setFiles] = useState([])
return (
<FilePond
files={files}
onupdatefiles={setFiles}
allowMultiple={true}
maxFiles={3}
server="/api/upload"
name="files"
labelIdle='Drag & Drop your files or <span class="filepond--label-action">Browse</span>'
/>
)
}
With Image Preview
yarn add filepond-plugin-image-preview
"use client"
import { FilePond, registerPlugin } from 'react-filepond'
import FilePondPluginImagePreview from 'filepond-plugin-image-preview'
import 'filepond/dist/filepond.min.css'
import 'filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css'
registerPlugin(FilePondPluginImagePreview)
export default function FilePondWithPreview() {
return (
<FilePond
allowMultiple={true}
server="/api/upload"
acceptedFileTypes={['image/*']}
/>
)
}
With Image Transform
yarn add filepond-plugin-image-transform filepond-plugin-image-resize
"use client"
import { FilePond, registerPlugin } from 'react-filepond'
import FilePondPluginImagePreview from 'filepond-plugin-image-preview'
import FilePondPluginImageResize from 'filepond-plugin-image-resize'
import FilePondPluginImageTransform from 'filepond-plugin-image-transform'
registerPlugin(
FilePondPluginImagePreview,
FilePondPluginImageResize,
FilePondPluginImageTransform
)
export default function FilePondWithTransform() {
return (
<FilePond
server="/api/upload"
acceptedFileTypes={['image/*']}
imageResizeTargetWidth={1920}
imageResizeTargetHeight={1080}
imageResizeMode="contain"
imageTransformOutputQuality={85}
/>
)
}
Advanced Configuration
"use client"
import { FilePond, registerPlugin } from 'react-filepond'
import FilePondPluginFileValidateSize from 'filepond-plugin-file-validate-size'
import FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type'
registerPlugin(
FilePondPluginFileValidateSize,
FilePondPluginFileValidateType
)
export default function AdvancedFilePond() {
return (
<FilePond
server={{
process: {
url: '/api/upload',
method: 'POST',
headers: {
'Authorization': 'Bearer token'
},
onload: (response) => JSON.parse(response).id,
onerror: (response) => response.data,
},
revert: '/api/upload',
restore: '/api/upload/',
load: '/api/upload/',
fetch: '/api/upload/',
}}
maxFileSize="5MB"
acceptedFileTypes={['image/png', 'image/jpeg']}
labelMaxFileSizeExceeded="File is too large"
labelMaxFileSize="Maximum file size is 5MB"
/>
)
}
Pros and Cons
Pros:
- Beautiful default UI
- Easy to implement
- Built-in image processing
- Excellent documentation
- Many plugins available
- Good accessibility
Cons:
- Opinionated UI (less customizable)
- Requires specific server response format
- Plugin-heavy for advanced features
- Can be challenging to style deeply
Best For
- Projects wanting beautiful default UI
- Applications needing image transformations
- Rapid prototyping
- Teams preferring minimal configuration
Cloudinary
Cloudinary is a complete media management platform offering upload, storage, transformation, optimization, and delivery through CDN.
Key Features
- Complete Solution - Upload, store, transform, deliver
- Automatic Optimization - Smart image/video optimization
- Transformations - On-the-fly media transformations
- AI Features - Auto-tagging, background removal
- Global CDN - Fast content delivery worldwide
- Video Support - Full video handling
- Generous Free Tier - Good for getting started
Installation
yarn add next-cloudinary
Setup
# .env.local
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=your_cloud_name
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_API_SECRET=your_api_secret
Upload Widget
"use client"
import { CldUploadWidget } from 'next-cloudinary'
export default function CloudinaryUpload() {
return (
<CldUploadWidget
uploadPreset="your_upload_preset"
onSuccess={(result, { widget }) => {
console.log('Uploaded:', result?.info)
widget.close()
}}
>
{({ open }) => (
<button onClick={() => open()}>
Upload an Image
</button>
)}
</CldUploadWidget>
)
}
With Custom UI
"use client"
import { CldUploadButton } from 'next-cloudinary'
export default function CloudinaryButton() {
return (
<CldUploadButton
uploadPreset="your_upload_preset"
onUpload={(result) => {
console.log('Upload complete:', result)
}}
className="px-4 py-2 bg-blue-500 text-white rounded"
>
Upload Image
</CldUploadButton>
)
}
Display Uploaded Images
import { CldImage } from 'next-cloudinary'
export default function DisplayImage() {
return (
<CldImage
src="your-public-id"
width="800"
height="600"
alt="Uploaded image"
crop="fill"
gravity="auto"
/>
)
}
Server-Side Upload
// app/api/upload/route.ts
import { v2 as cloudinary } from 'cloudinary'
cloudinary.config({
cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
})
export async function POST(request: Request) {
const formData = await request.formData()
const file = formData.get('file') as File
const bytes = await file.arrayBuffer()
const buffer = Buffer.from(bytes)
return new Promise((resolve, reject) => {
cloudinary.uploader.upload_stream(
{
folder: 'uploads',
resource_type: 'auto',
},
(error, result) => {
if (error) {
reject(error)
} else {
resolve(Response.json(result))
}
}
).end(buffer)
})
}
Pros and Cons
Pros:
- Complete media solution
- Powerful transformations
- Global CDN included
- Automatic optimization
- AI-powered features
- Generous free tier
Cons:
- Vendor lock-in
- Pricing scales with usage
- Overkill for simple use cases
- Learning curve for advanced features
Best For
- Media-heavy applications
- Projects needing image transformations
- Applications requiring video support
- Global applications needing CDN
AWS S3 Direct Upload
For projects already using AWS, implementing direct-to-S3 uploads with presigned URLs provides control and scalability.
Installation
yarn add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
Server-Side (Generate Presigned URL)
// app/api/presigned-url/route.ts
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
import { NextResponse } from 'next/server'
const s3Client = new S3Client({
region: process.env.AWS_REGION!,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
})
export async function POST(request: Request) {
const { filename, contentType } = await request.json()
const key = `uploads/${Date.now()}-${filename}`
const command = new PutObjectCommand({
Bucket: process.env.AWS_BUCKET_NAME!,
Key: key,
ContentType: contentType,
})
const presignedUrl = await getSignedUrl(s3Client, command, {
expiresIn: 3600, // 1 hour
})
return NextResponse.json({
presignedUrl,
key,
url: `https://${process.env.AWS_BUCKET_NAME}.s3.${process.env.AWS_REGION}.amazonaws.com/${key}`
})
}
Client-Side (Upload to S3)
"use client"
import { useState } from 'react'
export default function S3DirectUpload() {
const [uploading, setUploading] = useState(false)
const [uploadedUrl, setUploadedUrl] = useState<string>()
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
setUploading(true)
try {
// Get presigned URL
const response = await fetch('/api/presigned-url', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: file.name,
contentType: file.type,
}),
})
const { presignedUrl, url } = await response.json()
// Upload directly to S3
await fetch(presignedUrl, {
method: 'PUT',
body: file,
headers: {
'Content-Type': file.type,
},
})
setUploadedUrl(url)
alert('Upload successful!')
} catch (error) {
console.error('Upload failed:', error)
alert('Upload failed')
} finally {
setUploading(false)
}
}
return (
<div>
<input
type="file"
onChange={handleFileChange}
disabled={uploading}
/>
{uploading && <p>Uploading...</p>}
{uploadedUrl && (
<div>
<p>Uploaded successfully!</p>
<img src={uploadedUrl} alt="Uploaded" className="max-w-xs" />
</div>
)}
</div>
)
}
Pros and Cons
Pros:
- Full control over storage
- Highly scalable
- Cost-effective at scale
- No file size through your server
- Integrates with AWS ecosystem
Cons:
- More complex setup
- Requires AWS knowledge
- Manual CDN configuration
- More code to maintain
Best For
- AWS-based infrastructure
- Large file uploads
- Cost-conscious projects at scale
- Teams with AWS expertise
Comparison Table
| Feature | UploadThing | React Dropzone | Uppy | FilePond | Cloudinary | S3 Direct |
|---|---|---|---|---|---|---|
| Ease of Setup | ||||||
| Customization | ||||||
| Built-in UI | Yes | No | Yes | Yes | Yes | No |
| Storage | Included | DIY | DIY | DIY | Included | S3 |
| Transformations | Basic | No | Limited | Yes | Advanced | No |
| Free Tier | Generous | Free | Free | Free | Good | AWS Costs |
| TypeScript | ||||||
| Bundle Size | Small | Tiny | Large | Medium | Medium | Small |
Choosing the Right Solution
Decision Flowchart
Need a complete managed solution?
- Yes → UploadThing or Cloudinary
- No → Continue
Already using AWS?
- Yes → S3 Direct Upload
- No → Continue
Need advanced features (resumable, multiple sources)?
- Yes → Uppy
- No → Continue
Want beautiful default UI with minimal config?
- Yes → FilePond
- No → Continue
Need maximum customization?
- Yes → React Dropzone
- No → UploadThing (easiest overall)
By Use Case
Profile Picture Upload
- Primary: UploadThing
- Alternative: Cloudinary, FilePond
Document Management System
- Primary: Uppy
- Alternative: S3 Direct Upload
Media Gallery
- Primary: Cloudinary
- Alternative: UploadThing
Large File Uploads
- Primary: Uppy (with Tus)
- Alternative: S3 Direct Upload
Custom UI Requirements
- Primary: React Dropzone
- Alternative: S3 Direct Upload
Quick Prototyping
- Primary: UploadThing
- Alternative: FilePond
Integration Examples
With Form Libraries
import { useForm, Controller } from 'react-hook-form'
import { CldUploadWidget } from 'next-cloudinary'
interface FormData {
name: string
avatar: string
}
export default function FormWithUpload() {
const { control, handleSubmit } = useForm<FormData>()
const onSubmit = (data: FormData) => {
console.log(data)
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...control.register('name')} />
<Controller
name="avatar"
control={control}
render={({ field }) => (
<CldUploadWidget
uploadPreset="preset"
onSuccess={(result) => {
field.onChange(result?.info?.secure_url)
}}
>
{({ open }) => (
<button type="button" onClick={() => open()}>
Upload Avatar
</button>
)}
</CldUploadWidget>
)}
/>
<button type="submit">Submit</button>
</form>
)
}
With Database Storage
// After successful upload, save to database
import { prisma } from '@/lib/prisma'
async function saveFileMetadata(fileData: {
url: string
filename: string
size: number
type: string
userId: string
}) {
const file = await prisma.file.create({
data: {
url: fileData.url,
filename: fileData.filename,
size: fileData.size,
mimeType: fileData.type,
userId: fileData.userId,
uploadedAt: new Date(),
}
})
return file
}
Multiple Upload Strategies
"use client"
import { useState } from 'react'
import { useDropzone } from 'react-dropzone'
type Strategy = 'direct' | 'presigned' | 'api'
export default function MultiStrategyUpload() {
const [strategy, setStrategy] = useState<Strategy>('api')
const uploadViaApi = async (file: File) => {
const formData = new FormData()
formData.append('file', file)
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
})
return response.json()
}
const uploadViaPresigned = async (file: File) => {
const { presignedUrl } = await fetch('/api/presigned-url', {
method: 'POST',
body: JSON.stringify({ filename: file.name, contentType: file.type }),
}).then(r => r.json())
await fetch(presignedUrl, {
method: 'PUT',
body: file,
headers: { 'Content-Type': file.type },
})
}
const { getRootProps, getInputProps } = useDropzone({
onDrop: async (files) => {
for (const file of files) {
if (strategy === 'api') {
await uploadViaApi(file)
} else if (strategy === 'presigned') {
await uploadViaPresigned(file)
}
}
}
})
return (
<div>
<select value={strategy} onChange={(e) => setStrategy(e.target.value as Strategy)}>
<option value="api">Via API Route</option>
<option value="presigned">Via Presigned URL</option>
</select>
<div {...getRootProps()} className="border-2 border-dashed p-8 mt-4">
<input {...getInputProps()} />
<p>Drop files here</p>
</div>
</div>
)
}
Additional Resources
UploadThing
- Documentation: https://uploadthing.com/docs
- GitHub: https://github.com/pingdotgg/uploadthing
React Dropzone
- Documentation: https://react-dropzone.js.org/
- GitHub: https://github.com/react-dropzone/react-dropzone
Uppy
- Documentation: https://uppy.io/docs/
- GitHub: https://github.com/transloadit/uppy
FilePond
- Documentation: https://pqina.nl/filepond/
- GitHub: https://github.com/pqina/filepond
Cloudinary
- Documentation: https://cloudinary.com/documentation
- Next.js Integration: https://next.cloudinary.dev/
AWS S3
- Documentation: https://docs.aws.amazon.com/s3/
- SDK v3: https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/