Next.js
File Uploads & Storage
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
- Create a multi-file upload form with drag-and-drop support
- Implement server-side validation (type, size, content)
- Integrate AWS S3 or Cloudinary for storage
- Process images with Sharp (resize, optimize, create thumbnails)
- Add upload progress indicators
- Implement direct-to-S3 uploads with presigned URLs
- Create a file gallery displaying uploaded images
- Add file deletion functionality with confirmation