Handling File Uploads: S3, Presigned URLs, and Direct Browser Uploads
The naive approach — upload to your server, then to S3 — doubles bandwidth cost and adds latency. The right pattern bypasses your server entirely.
The Wrong Way
Browser → Your Server → S3
Your server becomes a bottleneck. Large files eat memory. You're paying for bandwidth twice.
The Right Way: Presigned URLs
Browser → Your Server (get presigned URL)
Browser → S3 directly (upload)
Browser → Your Server (confirm upload complete)
Your server only handles two tiny JSON requests. S3 handles the actual upload.
Server: Generate Presigned URL
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const s3 = new S3Client({ region: process.env.AWS_REGION });
app.post('/api/upload/presigned', async (req, res) => {
const { fileName, fileType, fileSize } = req.body;
// Validate before issuing URL
if (fileSize > 10 * 1024 * 1024) { // 10MB limit
return res.status(400).json({ error: 'File too large' });
}
const key = `uploads/${req.user.id}/${Date.now()}-${fileName}`;
const command = new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: key,
ContentType: fileType,
ContentLength: fileSize,
Metadata: { userId: req.user.id },
});
const presignedUrl = await getSignedUrl(s3, command, { expiresIn: 300 }); // 5 min
res.json({ presignedUrl, key });
});
Client: Upload Directly to S3
async function uploadFile(file: File) {
// Step 1: Get presigned URL from your server
const { presignedUrl, key } = await fetch('/api/upload/presigned', {
method: 'POST',
body: JSON.stringify({ fileName: file.name, fileType: file.type, fileSize: file.size }),
}).then(r => r.json());
// Step 2: Upload directly to S3
await fetch(presignedUrl, {
method: 'PUT',
body: file,
headers: { 'Content-Type': file.type },
});
// Step 3: Confirm with your server
await fetch('/api/upload/confirm', {
method: 'POST',
body: JSON.stringify({ key }),
});
return key;
}
S3 CORS Configuration
[
{
"AllowedOrigins": ["https://yourapp.com"],
"AllowedMethods": ["PUT", "POST"],
"AllowedHeaders": ["Content-Type", "Content-Length"],
"MaxAgeSeconds": 3000
}
]
Progress Tracking
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percent = (e.loaded / e.total) * 100;
setProgress(Math.round(percent));
}
});
xhr.open('PUT', presignedUrl);
xhr.setRequestHeader('Content-Type', file.type);
xhr.send(file);
File upload with S3 presigned URLs, progress tracking, and file validation are built into the AI SaaS Starter Kit.
Top comments (0)