How presigned URLs work
Your server generates a short-lived, signed URL that grants the client permission to PUT one specific object to one specific S3 key. The client uploads directly to S3. Your server never touches the bytes.
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const s3 = new S3Client({ region: 'ap-south-1' });
async function createUploadUrl(userId: string, contentType: string) {
const key = `uploads/${userId}/${crypto.randomUUID()}`;
const command = new PutObjectCommand({
Bucket: 'my-app-uploads',
Key: key,
ContentType: contentType,
});
const url = await getSignedUrl(s3, command, { expiresIn: 300 });
return { url, key };
}
Restricting what gets uploaded
The presigned URL locks the content type. If you sign for image/jpeg, the client cannot upload a .exe. But also set a content-length condition on the bucket policy to prevent multi-gigabyte abuse:
{
"Condition": {
"content-length-range": [0, 10485760]
}
}
That caps uploads at 10MB.
The client side
const { url } = await fetch('/api/upload-url', {
method: 'POST',
body: JSON.stringify({ contentType: file.type }),
}).then(r => r.json());
await fetch(url, {
method: 'PUT',
body: file,
headers: { 'Content-Type': file.type },
});
No multipart. No form data. Just a PUT with the raw file.
Post-upload verification
The presigned URL guarantees the upload goes to the right place, but it does not guarantee the content is valid. After upload, trigger a Lambda or queue a job to validate the file — check dimensions for images, scan for malware, transcode video. Do not trust the client.
Results
- Server memory during uploads: near zero (no buffering)
- Upload speed: 3x faster (client to S3 is closer than client to server to S3)
- Max concurrent uploads: limited only by S3, not by server capacity
