Implement secure file uploads with AWS S3 and Next.js. Learn presigned URLs, multipart uploads, image optimization, access control, and CDN integration with CloudFront.
# AWS S3 File Uploads Complete Guide
Build secure, scalable file upload systems with AWS S3 and Next.js using presigned URLs, multipart uploads, and CloudFront CDN.
## S3 Client Configuration
### AWS SDK Setup
```typescript
// lib/s3.ts
import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand } 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!,
},
});
const BUCKET_NAME = process.env.AWS_S3_BUCKET!;
export async function generateUploadUrl(
key: string,
contentType: string,
expiresIn = 3600
): Promise<string> {
const command = new PutObjectCommand({
Bucket: BUCKET_NAME,
Key: key,
ContentType: contentType,
});
return getSignedUrl(s3Client, command, { expiresIn });
}
export async function generateDownloadUrl(
key: string,
expiresIn = 3600
): Promise<string> {
const command = new GetObjectCommand({
Bucket: BUCKET_NAME,
Key: key,
});
return getSignedUrl(s3Client, command, { expiresIn });
}
export async function deleteFile(key: string): Promise<void> {
const command = new DeleteObjectCommand({
Bucket: BUCKET_NAME,
Key: key,
});
await s3Client.send(command);
}
export function getPublicUrl(key: string): string {
if (process.env.CLOUDFRONT_DOMAIN) {
return `https://${process.env.CLOUDFRONT_DOMAIN}/${key}`;
}
return `https://${BUCKET_NAME}.s3.${process.env.AWS_REGION}.amazonaws.com/${key}`;
}
```
## Presigned Upload API
### Generate Upload URL Endpoint
```typescript
// app/api/upload/presign/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { generateUploadUrl, getPublicUrl } from "@/lib/s3";
import { nanoid } from "nanoid";
import { z } from "zod";
const requestSchema = z.object({
filename: z.string().min(1).max(255),
contentType: z.string().regex(/^(image|video|application)\//),
size: z.number().max(50 * 1024 * 1024), // 50MB max
});
const ALLOWED_TYPES = [
"image/jpeg",
"image/png",
"image/webp",
"image/gif",
"application/pdf",
"video/mp4",
];
export async function POST(req: NextRequest) {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
try {
const body = await req.json();
const { filename, contentType, size } = requestSchema.parse(body);
if (!ALLOWED_TYPES.includes(contentType)) {
return NextResponse.json(
{ error: "File type not allowed" },
{ status: 400 }
);
}
// Generate unique key with user folder
const extension = filename.split(".").pop();
const key = `uploads/${session.user.id}/${nanoid()}.${extension}`;
const uploadUrl = await generateUploadUrl(key, contentType);
const publicUrl = getPublicUrl(key);
return NextResponse.json({
uploadUrl,
key,
publicUrl,
});
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json({ error: error.errors }, { status: 400 });
}
console.error("Presign error:", error);
return NextResponse.json(
{ error: "Failed to generate upload URL" },
{ status: 500 }
);
}
}
```
## React Upload Component
### File Upload Hook
```typescript
// hooks/useFileUpload.ts
"use client";
import { useState, useCallback } from "react";
interface UploadProgress {
loaded: number;
total: number;
percentage: number;
}
interface UploadResult {
key: string;
publicUrl: string;
}
export function useFileUpload() {
const [isUploading, setIsUploading] = useState(false);
const [progress, setProgress] = useState<UploadProgress | null>(null);
const [error, setError] = useState<Error | null>(null);
const upload = useCallback(async (file: File): Promise<UploadResult> => {
setIsUploading(true);
setProgress(null);
setError(null);
try {
// Get presigned URL
const presignResponse = await fetch("/api/upload/presign", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
filename: file.name,
contentType: file.type,
size: file.size,
}),
});
if (!presignResponse.ok) {
throw new Error("Failed to get upload URL");
}
const { uploadUrl, key, publicUrl } = await presignResponse.json();
// Upload to S3 with progress tracking
await new Promise<void>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener("progress", (event) => {
if (event.lengthComputable) {
setProgress({
loaded: event.loaded,
total: event.total,
percentage: Math.round((event.loaded / event.total) * 100),
});
}
});
xhr.addEventListener("load", () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve();
} else {
reject(new Error(`Upload failed: ${xhr.status}`));
}
});
xhr.addEventListener("error", () => reject(new Error("Upload failed")));
xhr.open("PUT", uploadUrl);
xhr.setRequestHeader("Content-Type", file.type);
xhr.send(file);
});
return { key, publicUrl };
} catch (err) {
const error = err instanceof Error ? err : new Error("Upload failed");
setError(error);
throw error;
} finally {
setIsUploading(false);
}
}, []);
return { upload, isUploading, progress, error };
}
```
### Upload Component
```typescript
// components/FileUpload.tsx
"use client";
import { useCallback, useState } from "react";
import { useDropzone } from "react-dropzone";
import { useFileUpload } from "@/hooks/useFileUpload";
interface FileUploadProps {
onUpload: (url: string) => void;
accept?: Record<string, string[]>;
maxSize?: number;
}
export function FileUpload({
onUpload,
accept = { "image/*": [".jpeg", ".jpg", ".png", ".webp"] },
maxSize = 10 * 1024 * 1024,
}: FileUploadProps) {
const { upload, isUploading, progress } = useFileUpload();
const [preview, setPreview] = useState<string | null>(null);
const onDrop = useCallback(
async (acceptedFiles: File[]) => {
const file = acceptedFiles[0];
if (!file) return;
// Create preview
if (file.type.startsWith("image/")) {
setPreview(URL.createObjectURL(file));
}
try {
const { publicUrl } = await upload(file);
onUpload(publicUrl);
} catch (error) {
console.error("Upload error:", error);
}
},
[upload, onUpload]
);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept,
maxSize,
multiple: false,
disabled: isUploading,
});
return (
<div
{...getRootProps()}
className={`
border-2 border-dashed rounded-lg p-8 text-center cursor-pointer
transition-colors duration-200
${isDragActive ? "border-blue-500 bg-blue-50" : "border-gray-300"}
${isUploading ? "opacity-50 cursor-not-allowed" : "hover:border-gray-400"}
`}
>
<input {...getInputProps()} />
{preview && (
<img
src={preview}
alt="Preview"
className="mx-auto mb-4 max-h-32 rounded"
/>
)}
{isUploading && progress ? (
<div className="space-y-2">
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
<div
className="h-full bg-blue-500 transition-all duration-300"
style={{ width: `${progress.percentage}%` }}
/>
</div>
<p className="text-sm text-gray-600">{progress.percentage}% uploaded</p>
</div>
) : (
<div>
<p className="text-gray-600">
{isDragActive
? "Drop the file here..."
: "Drag & drop a file, or click to select"}
</p>
<p className="text-sm text-gray-400 mt-2">
Max file size: {Math.round(maxSize / 1024 / 1024)}MB
</p>
</div>
)}
</div>
);
}
```
## Image Optimization
```typescript
// lib/image-processing.ts
import sharp from "sharp";
export async function optimizeImage(
buffer: Buffer,
options: {
width?: number;
height?: number;
quality?: number;
format?: "webp" | "jpeg" | "png";
} = {}
): Promise<Buffer> {
const { width = 1200, height, quality = 80, format = "webp" } = options;
let pipeline = sharp(buffer).resize(width, height, {
fit: "inside",
withoutEnlargement: true,
});
switch (format) {
case "webp":
pipeline = pipeline.webp({ quality });
break;
case "jpeg":
pipeline = pipeline.jpeg({ quality, progressive: true });
break;
case "png":
pipeline = pipeline.png({ compressionLevel: 9 });
break;
}
return pipeline.toBuffer();
}
```
This S3 guide covers presigned uploads, progress tracking, drag-and-drop, and image optimization.This aws prompt is ideal for developers working on:
By using this prompt, you can save hours of manual coding and ensure best practices are followed from the start. It's particularly valuable for teams looking to maintain consistency across their aws implementations.
Yes! All prompts on Antigravity AI Directory are free to use for both personal and commercial projects. No attribution required, though it's always appreciated.
This prompt works excellently with Claude, ChatGPT, Cursor, GitHub Copilot, and other modern AI coding assistants. For best results, use models with large context windows.
You can modify the prompt by adding specific requirements, constraints, or preferences. For aws projects, consider mentioning your framework version, coding style, and any specific libraries you're using.