Loading...
Loading...
> Implement secure file uploads with dropzone components, validation, and cloud storage integration.
Drag-and-drop file upload components with image preview, file validation (size, type, count), progress indicators, error handling, multiple file support, and cloud storage integration.
DESC: Install the required packages for uploads and S3/R2 storage
1$npm install react-dropzone @aws-sdk/client-s3
DESC: Add cloud storage credentials to your .env.local
1$# For AWS S32$AWS_REGION="us-east-1"3$AWS_ACCESS_KEY_ID="your-access-key"4$AWS_SECRET_ACCESS_KEY="your-secret-key"5$S3_BUCKET="your-bucket-name"6$S3_PUBLIC_URL="https://your-bucket.s3.amazonaws.com"78$# For Cloudflare R2 (S3-compatible)9$AWS_REGION="auto"10$S3_ENDPOINT="https://<account-id>.r2.cloudflarestorage.com"11$AWS_ACCESS_KEY_ID="your-r2-access-key"12$AWS_SECRET_ACCESS_KEY="your-r2-secret-key"13$S3_BUCKET="your-bucket-name"14$S3_PUBLIC_URL="https://pub-xxx.r2.dev"
Create an API route to handle file uploads with validation
1// src/app/api/upload/route.ts23import { NextRequest, NextResponse } from "next/server";4import { auth } from "@/lib/auth";56// Allowed MIME types7const ALLOWED_TYPES = [8 "image/jpeg",9 "image/png",10 "image/gif",11 "image/webp",12 "application/pdf",13];1415// Max file size (5MB)16const MAX_SIZE = 5 * 1024 * 1024;1718export async function POST(request: NextRequest) {19 try {20 // Check authentication21 const session = await auth();22 if (!session?.user) {23 return NextResponse.json(24 { error: "Unauthorized" },25 { status: 401 }26 );27 }2829 const formData = await request.formData();30 const file = formData.get("file") as File | null;3132 if (!file) {33 return NextResponse.json(34 { error: "No file provided" },35 { status: 400 }36 );37 }3839 // Validate file type40 if (!ALLOWED_TYPES.includes(file.type)) {41 return NextResponse.json(42 { error: "Invalid file type. Allowed: JPEG, PNG, GIF, WebP, PDF" },43 { status: 400 }44 );45 }4647 // Validate file size48 if (file.size > MAX_SIZE) {49 return NextResponse.json(50 { error: "File too large. Maximum size is 5MB" },51 { status: 400 }52 );53 }5455 // Convert to buffer for processing56 const bytes = await file.arrayBuffer();57 const buffer = Buffer.from(bytes);5859 // Generate unique filename60 const timestamp = Date.now();61 const extension = file.name.split(".").pop();62 const filename = `${session.user.id}-${timestamp}.${extension}`;6364 // Upload to S3/R2 (using AWS SDK v3)65 const { S3Client, PutObjectCommand } = await import("@aws-sdk/client-s3");6667 const s3 = new S3Client({68 region: process.env.AWS_REGION || "auto",69 endpoint: process.env.S3_ENDPOINT, // For R2: https://<account>.r2.cloudflarestorage.com70 credentials: {71 accessKeyId: process.env.AWS_ACCESS_KEY_ID!,72 secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,73 },74 });7576 await s3.send(new PutObjectCommand({77 Bucket: process.env.S3_BUCKET!,78 Key: `uploads/${filename}`,79 Body: buffer,80 ContentType: file.type,81 }));8283 const fileUrl = `${process.env.S3_PUBLIC_URL}/uploads/${filename}`;8485 return NextResponse.json({86 success: true,87 file: {88 name: filename,89 originalName: file.name,90 size: file.size,91 type: file.type,92 url: fileUrl,93 },94 });95 } catch (_) {96 console.error("Upload error:", error);97 return NextResponse.json(98 { error: "Upload failed" },99 { status: 500 }100 );101 }102}
Handle the upload in your component with progress tracking
1"use client";23import { useState } from "react";4import { useDropzone } from "react-dropzone";5import { Button } from "@/components/ui/button";67export function FileUploadForm() {8 const [files, setFiles] = useState<File[]>([]);9 const [uploading, setUploading] = useState(false);10 const [progress, setProgress] = useState(0);11 const [error, setError] = useState<string | null>(null);1213 const { getRootProps, getInputProps, isDragActive } = useDropzone({14 onDrop: (acceptedFiles) => {15 setFiles(acceptedFiles);16 setError(null);17 },18 maxFiles: 5,19 maxSize: 5 * 1024 * 1024,20 accept: {21 "image/*": [".png", ".jpg", ".jpeg", ".gif", ".webp"],22 },23 });2425 const handleUpload = async () => {26 if (files.length === 0) return;2728 setUploading(true);29 setError(null);30 setProgress(0);3132 try {33 for (let i = 0; i < files.length; i++) {34 const file = files[i];35 const formData = new FormData();36 formData.append("file", file);3738 const response = await fetch("/api/upload", {39 method: "POST",40 body: formData,41 });4243 if (!response.ok) {44 const data = await response.json();45 throw new Error(data.error || "Upload failed");46 }4748 setProgress(((i + 1) / files.length) * 100);49 }5051 setFiles([]);52 } catch (err) {53 setError(err instanceof Error ? err.message : "Upload failed");54 } finally {55 setUploading(false);56 }57 };5859 return (60 <div className="space-y-4">61 <div62 {...getRootProps()}63 className={cn('border-2 border-dashed p-6 text-center cursor-pointer transition-colors', mode.radius, mode.state.hover.card)}64 >65 <input {...getInputProps()} />66 <p className="font-mono text-sm text-muted-foreground">67 {isDragActive ? "Drop files here..." : "Drag & drop files or click to browse"}68 </p>69 </div>7071 {uploading && (72 <div className="space-y-2">73 <div className="h-2 bg-muted overflow-hidden">74 <div75 className="h-full bg-primary transition-all"76 style={{ width: `${progress}%` }}77 />78 </div>79 <p className="font-mono text-sm text-muted-foreground text-center">80 Uploading... {Math.round(progress)}%81 </p>82 </div>83 )}8485 {error && (86 <p className="font-mono text-xs text-destructive">{error}</p>87 )}8889 <Button90 onClick={handleUpload}91 disabled={files.length === 0 || uploading}92 className="w-full"93 >94 {uploading ? "Uploading..." : "Upload Files"}95 </Button>96 </div>97 );98}
Common validation configurations for different use cases
1// Profile Avatar2<Dropzone3 maxFiles={1}4 maxSize={2 * 1024 * 1024} // 2MB5 accept={{6 "image/*": [".png", ".jpg", ".jpeg", ".webp"],7 }}8/>910// Document Upload11<Dropzone12 maxFiles={10}13 maxSize={10 * 1024 * 1024} // 10MB14 accept={{15 "application/pdf": [".pdf"],16 "application/msword": [".doc"],17 "application/vnd.openxmlformats-officedocument.wordprocessingml.document": [".docx"],18 }}19/>2021// Image Gallery22<Dropzone23 maxFiles={20}24 maxSize={5 * 1024 * 1024} // 5MB25 accept={{26 "image/*": [".png", ".jpg", ".jpeg", ".gif", ".webp"],27 }}28/>
[ERROR]: File too large (413 Payload Too Large)
Solution: Adjust maxSize in dropzone config and API validation
// Client (react-dropzone)
maxSize: 10 * 1024 * 1024 // 10MB
// API route validation
const MAX_SIZE = 10 * 1024 * 1024;
if (file.size > MAX_SIZE) {
return NextResponse.json({ error: "File too large" }, { status: 400 });
}[ERROR]: Invalid file type rejected
Solution: Verify accept prop matches server-side validation
// Client and server must match
// Client
accept={{ "image/*": [".png", ".jpg"] }}
// Server
const ALLOWED_TYPES = ["image/png", "image/jpeg"];
if (!ALLOWED_TYPES.includes(file.type)) { /* reject */ }[ERROR]: Upload fails silently (no error message)
Solution: Check S3/R2 credentials and bucket configuration
# Verify credentials in .env.local
AWS_ACCESS_KEY_ID="..."
AWS_SECRET_ACCESS_KEY="..."
S3_BUCKET="your-bucket"
# Test bucket access
# Check bucket is public or has correct CORS policy[ERROR]: CORS error when uploading to S3
Solution: Add CORS policy to your S3/R2 bucket
// S3 Bucket CORS Configuration
[
{
"AllowedOrigins": ["http://localhost:3000", "https://yourdomain.com"],
"AllowedMethods": ["GET", "POST", "PUT"],
"AllowedHeaders": ["*"]
}
]