Next.js

File Uploads & Storage

25 min Lesson 29 of 40

File Uploads & Storage

Handling file uploads securely and efficiently is essential for modern web applications. This lesson covers file upload handling, cloud storage integration, image processing, and best practices for managing user-uploaded content.

Basic File Upload with FormData

Start with a simple file upload implementation:

Upload Form Component

// components/UploadForm.tsx 'use client'; import { useState } from 'react'; export default function UploadForm() { const [file, setFile] = useState<File | null>(null); const [uploading, setUploading] = useState(false); const [message, setMessage] = useState(''); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!file) { setMessage('Please select a file'); return; } setUploading(true); const formData = new FormData(); formData.append('file', file); try { const response = await fetch('/api/upload', { method: 'POST', body: formData, }); const result = await response.json(); if (response.ok) { setMessage('File uploaded successfully!'); setFile(null); } else { setMessage(result.error || 'Upload failed'); } } catch (error) { setMessage('Upload error'); } finally { setUploading(false); } }; return ( <form onSubmit={handleSubmit}> <input type="file" onChange={(e) => setFile(e.target.files?.[0] || null)} disabled={uploading} /> <button type="submit" disabled={!file || uploading}> {uploading ? 'Uploading...' : 'Upload'} </button> {message && <p>{message}</p>} </form> ); }

API Route for File Upload

// app/api/upload/route.ts import { writeFile } from 'fs/promises'; import { NextRequest } from 'next/server'; import path from 'path'; export async function POST(request: NextRequest) { try { const formData = await request.formData(); const file = formData.get('file') as File; if (!file) { return Response.json( { error: 'No file provided' }, { status: 400 } ); } // Validate file type const allowedTypes = ['image/jpeg', 'image/png', 'image/webp']; if (!allowedTypes.includes(file.type)) { return Response.json( { error: 'Invalid file type' }, { status: 400 } ); } // Validate file size (5MB max) const maxSize = 5 * 1024 * 1024; if (file.size > maxSize) { return Response.json( { error: 'File too large' }, { status: 400 } ); } // Generate unique filename const bytes = await file.arrayBuffer(); const buffer = Buffer.from(bytes); const filename = `${Date.now()}-${file.name}`; const filepath = path.join(process.cwd(), 'public/uploads', filename); // Save file to disk await writeFile(filepath, buffer); return Response.json({ success: true, filename, url: `/uploads/${filename}`, }); } catch (error) { console.error('Upload error:', error); return Response.json( { error: 'Upload failed' }, { status: 500 } ); } }
Security Warning: Always validate file types, sizes, and content. Never trust client-side validation alone. Sanitize filenames to prevent directory traversal attacks. Store uploads outside the web root when possible.

Multiple File Uploads

Handle multiple files simultaneously:

// components/MultiUpload.tsx 'use client'; import { useState } from 'react'; export default function MultiUpload() { const [files, setFiles] = useState<FileList | null>(null); const [uploading, setUploading] = useState(false); const [results, setResults] = useState<any[]>([]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!files || files.length === 0) return; setUploading(true); const formData = new FormData(); // Append all files Array.from(files).forEach((file) => { formData.append('files', file); }); try { const response = await fetch('/api/upload-multiple', { method: 'POST', body: formData, }); const data = await response.json(); setResults(data.files); } catch (error) { console.error(error); } finally { setUploading(false); } }; return ( <form onSubmit={handleSubmit}> <input type="file" multiple onChange={(e) => setFiles(e.target.files)} /> <button type="submit" disabled={uploading}> Upload {files?.length || 0} files </button> {results.length > 0 && ( <ul> {results.map((file, i) => ( <li key={i}> {file.filename}: {file.success ? '✓' : '✗'} </li> ))} </ul> )} </form> ); }

AWS S3 Integration

Store files in AWS S3 for scalable cloud storage:

Setup AWS SDK

# Install AWS SDK npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner # Environment variables AWS_ACCESS_KEY_ID=your_access_key AWS_SECRET_ACCESS_KEY=your_secret_key AWS_REGION=us-east-1 AWS_S3_BUCKET=your-bucket-name

S3 Upload Utility

// lib/s3.ts import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; 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 uploadToS3( file: Buffer, filename: string, contentType: string ) { const command = new PutObjectCommand({ Bucket: process.env.AWS_S3_BUCKET!, Key: filename, Body: file, ContentType: contentType, }); await s3Client.send(command); return `https://${process.env.AWS_S3_BUCKET}.s3.${process.env.AWS_REGION}.amazonaws.com/${filename}`; } export async function getPresignedUrl(key: string, expiresIn = 3600) { const command = new GetObjectCommand({ Bucket: process.env.AWS_S3_BUCKET!, Key: key, }); return await getSignedUrl(s3Client, command, { expiresIn }); }

S3 Upload API Route

// app/api/upload-s3/route.ts import { NextRequest } from 'next/server'; import { uploadToS3 } from '@/lib/s3'; export async function POST(request: NextRequest) { try { const formData = await request.formData(); const file = formData.get('file') as File; if (!file) { return Response.json({ error: 'No file' }, { status: 400 }); } const bytes = await file.arrayBuffer(); const buffer = Buffer.from(bytes); const filename = `${Date.now()}-${file.name}`; const url = await uploadToS3(buffer, filename, file.type); return Response.json({ success: true, url, filename, }); } catch (error) { console.error('S3 upload error:', error); return Response.json({ error: 'Upload failed' }, { status: 500 }); } }

Cloudinary Integration

Cloudinary provides image/video hosting with built-in transformations:

Setup Cloudinary

# Install Cloudinary SDK npm install cloudinary # Environment variables CLOUDINARY_CLOUD_NAME=your_cloud_name CLOUDINARY_API_KEY=your_api_key CLOUDINARY_API_SECRET=your_api_secret NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=your_cloud_name

Cloudinary Upload Utility

// lib/cloudinary.ts import { v2 as cloudinary } from 'cloudinary'; cloudinary.config({ cloud_name: process.env.CLOUDINARY_CLOUD_NAME, api_key: process.env.CLOUDINARY_API_KEY, api_secret: process.env.CLOUDINARY_API_SECRET, }); export async function uploadToCloudinary( fileBuffer: Buffer, folder: string = 'uploads' ) { return new Promise((resolve, reject) => { cloudinary.uploader .upload_stream( { folder, resource_type: 'auto', }, (error, result) => { if (error) reject(error); else resolve(result); } ) .end(fileBuffer); }); }

Cloudinary Upload API

// app/api/upload-cloudinary/route.ts import { NextRequest } from 'next/server'; import { uploadToCloudinary } from '@/lib/cloudinary'; export async function POST(request: NextRequest) { try { const formData = await request.formData(); const file = formData.get('file') as File; const bytes = await file.arrayBuffer(); const buffer = Buffer.from(bytes); const result = await uploadToCloudinary(buffer, 'user-uploads'); return Response.json({ success: true, url: result.secure_url, publicId: result.public_id, }); } catch (error) { return Response.json({ error: 'Upload failed' }, { status: 500 }); } }

Image Processing with Sharp

Optimize and transform images server-side:

# Install Sharp npm install sharp // lib/image-processing.ts import sharp from 'sharp'; export async function processImage( buffer: Buffer, options: { width?: number; height?: number; quality?: number; format?: 'jpeg' | 'png' | 'webp'; } ) { let image = sharp(buffer); // Resize if dimensions provided if (options.width || options.height) { image = image.resize(options.width, options.height, { fit: 'cover', position: 'center', }); } // Convert format const format = options.format || 'webp'; const quality = options.quality || 80; if (format === 'jpeg') { image = image.jpeg({ quality }); } else if (format === 'png') { image = image.png({ quality }); } else { image = image.webp({ quality }); } return await image.toBuffer(); } export async function createThumbnail(buffer: Buffer, size: number = 200) { return await sharp(buffer) .resize(size, size, { fit: 'cover' }) .webp({ quality: 80 }) .toBuffer(); }

API Route with Image Processing

// app/api/upload-optimized/route.ts import { NextRequest } from 'next/server'; import { processImage, createThumbnail } from '@/lib/image-processing'; import { uploadToS3 } from '@/lib/s3'; export async function POST(request: NextRequest) { try { const formData = await request.formData(); const file = formData.get('file') as File; const bytes = await file.arrayBuffer(); const buffer = Buffer.from(bytes); // Create optimized version const optimized = await processImage(buffer, { width: 1200, quality: 85, format: 'webp', }); // Create thumbnail const thumbnail = await createThumbnail(buffer, 300); // Upload both to S3 const filename = `${Date.now()}-${file.name}`; const [mainUrl, thumbUrl] = await Promise.all([ uploadToS3(optimized, `images/${filename}.webp', 'image/webp'), uploadToS3(thumbnail, `thumbnails/${filename}.webp', 'image/webp'), ]); return Response.json({ success: true, urls: { main: mainUrl, thumbnail: thumbUrl, }, }); } catch (error) { return Response.json({ error: 'Processing failed' }, { status: 500 }); } }

Drag and Drop Upload

Enhance UX with drag-and-drop functionality:

// components/DragDropUpload.tsx 'use client'; import { useState, DragEvent } from 'react'; export default function DragDropUpload() { const [isDragging, setIsDragging] = useState(false); const [files, setFiles] = useState<File[]>([]); const handleDrag = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); }; const handleDragIn = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(true); }; const handleDragOut = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); }; const handleDrop = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); const droppedFiles = Array.from(e.dataTransfer.files); setFiles((prev) => [...prev, ...droppedFiles]); }; return ( <div onDragEnter={handleDragIn} onDragLeave={handleDragOut} onDragOver={handleDrag} onDrop={handleDrop} style={{ border: `2px dashed ${isDragging ? '#4f46e5' : '#ccc'}`, padding: '40px', textAlign: 'center', background: isDragging ? '#f0f0f0' : 'white', }} > {files.length === 0 ? ( <p>Drag and drop files here, or click to select</p> ) : ( <ul> {files.map((file, i) => ( <li key={i}>{file.name}</li> ))} </ul> )} <input type="file" multiple onChange={(e) => { if (e.target.files) { setFiles(Array.from(e.target.files)); } }} style={{ display: 'none' }} id="file-input" /> <label htmlFor="file-input"> <button type="button">Select Files</button> </label> </div> ); }

Upload Progress Tracking

Show upload progress to users:

// components/UploadWithProgress.tsx 'use client'; import { useState } from 'react'; export default function UploadWithProgress() { const [progress, setProgress] = useState(0); const [uploading, setUploading] = useState(false); const handleUpload = async (file: File) => { setUploading(true); setProgress(0); const xhr = new XMLHttpRequest(); // Track upload progress xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable) { const percentComplete = (e.loaded / e.total) * 100; setProgress(Math.round(percentComplete)); } }); xhr.addEventListener('load', () => { setUploading(false); if (xhr.status === 200) { console.log('Upload complete'); } }); xhr.open('POST', '/api/upload'); const formData = new FormData(); formData.append('file', file); xhr.send(formData); }; return ( <div> <input type="file" onChange={(e) => { const file = e.target.files?.[0]; if (file) handleUpload(file); }} /> {uploading && ( <div> <div style={{ width: '100%', height: '20px', background: '#f0f0f0', borderRadius: '10px', overflow: 'hidden', }}> <div style={{ width: `${progress}%`, height: '100%', background: '#4f46e5', transition: 'width 0.3s', }} /> </div> <p>{progress}% uploaded</p> </div> )} </div> ); }

Direct Upload to S3 with Presigned URLs

Allow client-side uploads directly to S3 for better performance:

// app/api/presigned-url/route.ts import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; 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_S3_BUCKET!, Key: key, ContentType: contentType, }); const presignedUrl = await getSignedUrl(s3Client, command, { expiresIn: 3600, }); return Response.json({ uploadUrl: presignedUrl, key, }); } // Client component // components/DirectS3Upload.tsx const handleUpload = async (file: File) => { // Get presigned URL const response = await fetch('/api/presigned-url', { method: 'POST', body: JSON.stringify({ filename: file.name, contentType: file.type, }), }); const { uploadUrl, key } = await response.json(); // Upload directly to S3 await fetch(uploadUrl, { method: 'PUT', body: file, headers: { 'Content-Type': file.type, }, }); console.log('Uploaded to S3:', key); };
Best Practices: Always validate file types and sizes server-side. Use presigned URLs for direct uploads to reduce server load. Generate thumbnails asynchronously. Store file metadata in your database. Implement virus scanning for user uploads.

Exercise: Build a File Upload System

  1. Create a multi-file upload form with drag-and-drop support
  2. Implement server-side validation (type, size, content)
  3. Integrate AWS S3 or Cloudinary for storage
  4. Process images with Sharp (resize, optimize, create thumbnails)
  5. Add upload progress indicators
  6. Implement direct-to-S3 uploads with presigned URLs
  7. Create a file gallery displaying uploaded images
  8. Add file deletion functionality with confirmation