Image Upload
This guide explains how to use the image upload system in our application using AWS S3 for storage and pre-signed URLs for secure uploads.
Setup
Prerequisites
- AWS Account with S3 access
- AWS S3 bucket created
- AWS credentials configured
Environment Variables
# AWS Configuration
AWS_ACCESS_KEY_ID=your_access_key_id
AWS_SECRET_ACCESS_KEY=your_secret_access_key
AWS_REGION=your_region
S3_UPLOAD_BUCKET=your_bucket_name
Components
Image Upload Component
A reusable component for handling image uploads with preview:
type ImageUploadProps = {
value: string | null | undefined
onChange: (url?: string, name?: string) => void
type?: "profile" | "logo"
disabled?: boolean
disableActions?: boolean
}
export function ImageUpload({
value,
onChange,
type = "profile",
disabled = false,
disableActions = false,
}: ImageUploadProps) {
const [isUploading, setIsUploading] = useState<boolean>(false)
const handleFileDrop = async (file: File) => {
try {
setIsUploading(true)
const data = await getPreSignedUrl(file)
if (!data?.data) {
return toast.error("Too many requests")
}
const uploadSuccessful = await uploadFileToS3(
data.data.preSignedUrl,
file
)
if (uploadSuccessful) {
onChange(data.data.imageUrl, file.name)
}
} catch (error) {
toast.error(error)
} finally {
setIsUploading(false)
}
}
// ... rest of component
}
Features
Drag & Drop
Support for drag and drop file uploads using react-dropzone
Image Preview
Real-time preview of uploaded images with Next.js Image component
File Validation
Validates file types and shows appropriate error messages
Loading States
Visual feedback during upload process with loading indicators
Upload Process
1. Get Pre-signed URL
Request a pre-signed URL from the server:
async function getPreSignedUrl(file: File) {
try {
const res = await fetch("/api/image-upload", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
filename: file.name,
filetype: file.type,
}),
})
if (!res.ok) {
throw new Error((await res.json()).message)
}
return await res.json()
} catch (error) {
throw Error(error?.message)
}
}
2. Upload to S3
Upload the file directly to S3 using the pre-signed URL:
export const uploadFileToS3 = async (url: string, file: File) => {
try {
const buffer = await file.arrayBuffer()
await new Promise<void>((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open("PUT", url, true)
xhr.setRequestHeader("Content-Type", file.type)
xhr.setRequestHeader("Cache-Control", "max-age=63072000")
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
xhr.status >= 200 && xhr.status < 300
? resolve()
: reject()
}
}
xhr.send(buffer)
})
return true
} catch (error) {
return false
}
}
API Route
The /api/image-upload
route handles the server-side logic for generating pre-signed URLs and managing S3 uploads.
API Route Handler
Server-side implementation for secure image uploads:
export async function POST(req: NextRequest) {
try {
// 1. Get request data and authenticate user
const { filename, filetype } = await req.json()
const { user } = await getCurrentUser()
if (!user) {
return responses.notAuthenticatedResponse()
}
// 2. Check rate limit
const identifier = `ratelimit:image-upload:${user.id}`
const { success } = await ratelimit.limit(identifier)
if (!success) {
return responses.tooManyRequestsResponse()
}
// 3. Generate unique filename
const generatedFilename = getFilename(filename)
// 4. Configure S3 parameters
const params = {
Bucket: process.env.S3_UPLOAD_BUCKET,
Key: `${user.id}/${generatedFilename}`,
ContentType: filetype,
CacheControl: "max-age=63072000",
}
// 5. Generate pre-signed URL
const preSignedUrl = await getSignedUrl(
awsClient,
new PutObjectCommand(params),
{ expiresIn: 60 * 60 }
)
// 6. Create the final image URL
const imageUrl = `https://${process.env.S3_UPLOAD_BUCKET}.s3.amazonaws.com/${user.id}/${generatedFilename}`
return responses.successResponse(
{ preSignedUrl, imageUrl },
"Image uploaded successfully"
)
} catch (error: any) {
return responses.internalServerErrorResponse(error.message)
}
}
Key Features
Secure File Names
Generates unique file names using MD5 hashing and timestamps
Rate Limiting
Prevents abuse with Upstash Redis-based rate limiting
User Isolation
Stores files in user-specific S3 directories
Caching Strategy
Implements proper cache control headers
Filename Generation
Secure filename generation to prevent conflicts:
const getFilename = (originalName: string) => {
const originalExtension = path.extname(originalName)
const currentTime = new Date().getTime().toString()
const hash = crypto
.createHash("md5")
.update(currentTime)
.digest("hex")
return `${hash}${originalExtension}`
}
Rate Limiting Setup
Configure rate limiting with Upstash Redis:
const ratelimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(
RATE_LIMIT_5,
RATE_LIMIT_1_MINUTE
),
})
AWS Configuration
AWS Client Setup
Configure the AWS S3 client:
import { S3Client } from "@aws-sdk/client-s3"
export const awsClient = new S3Client({
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
region: process.env.AWS_REGION,
})
Best Practices
Security
- Use pre-signed URLs for secure uploads
- Implement proper file validation
- Set appropriate CORS policies
- Use environment variables for credentials
User Experience
- Show upload progress indicators
- Provide clear error messages
- Support drag and drop
- Preview uploaded images