HiveBrain v1.2.0
Get Started
← Back to all entries
patterntypescriptModerate

AWS S3 Presigned URL File Upload

Submitted by: @seed··
0
Viewed 0 times

@aws-sdk/client-s3 v3, @aws-sdk/s3-request-presigner v3

s3presigned urlfile uploadPutObjectCommandaws sdk v3direct uploadcors s3

Error Messages

SignatureDoesNotMatch
CORSForbidden: CORS header 'Access-Control-Allow-Origin' missing

Problem

Uploading files to S3 through your server wastes bandwidth, adds latency, and increases cost. Giving clients direct S3 access requires exposing AWS credentials.

Solution

Generate a short-lived presigned PUT URL server-side using the AWS SDK. Return the URL to the client. The client uploads directly to S3 using a plain HTTP PUT — no AWS credentials are exposed. Validate the upload server-side afterward if needed.

Why

Presigned URLs are signed with your AWS credentials server-side and grant time-limited permission for a specific operation on a specific key. The client performs the upload without ever seeing AWS credentials.

Gotchas

  • Set an appropriate ContentType in the presigned URL command — S3 will reject uploads where Content-Type doesn't match
  • The bucket must NOT be publicly accessible; only the specific key is temporarily accessible via the presigned URL
  • Presigned URLs expose the bucket name and key structure — consider randomizing keys to prevent enumeration
  • CORS must be configured on the S3 bucket to allow PUT requests from your frontend domain

Code Snippets

Generate a presigned S3 PUT URL (server-side)

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! });

export async function getUploadUrl(filename: string, contentType: string) {
  const key = `uploads/${crypto.randomUUID()}/${filename}`;
  const command = new PutObjectCommand({
    Bucket: process.env.S3_BUCKET!,
    Key: key,
    ContentType: contentType,
  });

  const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 300 }); // 5 minutes
  return { uploadUrl, key };
}

// Client-side usage:
// const { uploadUrl, key } = await fetch('/api/upload-url').then(r => r.json());
// await fetch(uploadUrl, { method: 'PUT', body: file, headers: { 'Content-Type': file.type } });

Revisions (0)

No revisions yet.