Instead of uploading images through the backend (consuming bandwidth/risking timeout), we use a two-step approach:
Benefits:
Key configurations:

[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
"AllowedOrigins": ["*"],
"ExposeHeaders": ["ETag"]
}
]

Note: In production, replace "*" with specific domains.
Function: GetUploadUrlFunction
API Path: POST /upload-url
Authentication: Cognito JWT required
POST {API_BASE}/upload-url with JWT tokenuploadUrl: Temporary S3 upload URLkey: Object path in S3expiresIn: 900 seconds (15 minutes){
"uploadUrl": "https://s3.ap-southeast-1.amazonaws.com/...",
"key": "articles/<articleId>/raw/<uuid>.jpg",
"articleId": "<uuid>",
"expiresIn": 900
}
import boto3
import uuid
from datetime import datetime
s3_client = boto3.client('s3')
BUCKET_NAME = os.environ['ARTICLE_IMAGES_BUCKET']
def lambda_handler(event, context):
# Get user info from Cognito
user_id = event['requestContext']['authorizer']['claims']['sub']
# Generate unique key
article_id = str(uuid.uuid4())
file_key = f"articles/{article_id}/raw/{uuid.uuid4()}.jpg"
# Generate pre-signed URL
upload_url = s3_client.generate_presigned_url(
'put_object',
Params={
'Bucket': BUCKET_NAME,
'Key': file_key,
'ContentType': 'image/jpeg'
},
ExpiresIn=900 # 15 minutes
)
return {
'statusCode': 200,
'body': json.dumps({
'uploadUrl': upload_url,
'key': file_key,
'articleId': article_id,
'expiresIn': 900
})
}
// Step 1: Get pre-signed URL
const getUploadUrl = async () => {
const response = await fetch(`${API_BASE}/upload-url`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
});
return await response.json();
};
// Step 2: Upload file directly to S3
const uploadImage = async (file) => {
// Get upload URL
const { uploadUrl, key, articleId } = await getUploadUrl();
// Upload to S3
const uploadResponse = await fetch(uploadUrl, {
method: 'PUT',
headers: {
'Content-Type': file.type
},
body: file
});
if (uploadResponse.ok) {
console.log('Upload successful!');
return { key, articleId };
} else {
throw new Error('Upload failed');
}
};
// Usage
const handleFileSelect = async (event) => {
const file = event.target.files[0];
try {
const result = await uploadImage(file);
console.log('Image uploaded:', result);
} catch (error) {
console.error('Upload error:', error);
}
};
The Lambda function needs s3:PutObject permission:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:PutObjectAcl"
],
"Resource": "arn:aws:s3:::article-images-bucket/*"
}
]
}
Note: Frontend doesn’t need AWS credentials because it uses pre-signed URLs.
Causes:
Solution:
Causes:
Solution:
Cause:
Solution:

CloudWatch Metrics to track:
Strategies: