mdashikjs/blog
All posts
Secure File Uploads with AWS S3 Presigned URLs
Cloud

Secure File Uploads with AWS S3 Presigned URLs

Cloud4 min

Secure File Uploads with AWS S3 Presigned URLs

Routing file uploads through your server is a bottleneck. Presigned URLs let clients upload directly to S3 while your server keeps full control over what gets uploaded and where.

AWSS3Node.jsFile Upload
Share:

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
MA

Written by Md Ashik

Senior Software Engineer building reliable backends. I write about the practical tradeoffs behind shipping software that holds up in production.