go 26 lines · 1 tab

Presigned S3 upload URLs (AWS SDK v2)

Leah Thompson Jan 2026
1 tab
package files

import (
  "context"
  "time"

  "github.com/aws/aws-sdk-go-v2/aws"
  "github.com/aws/aws-sdk-go-v2/service/s3"
)

type Presigner struct {
  bucket string
  p      *s3.PresignClient
}

func NewPresigner(cfg aws.Config, bucket string) *Presigner {
  return &Presigner{bucket: bucket, p: s3.NewPresignClient(s3.NewFromConfig(cfg))}
}

func (p *Presigner) PresignPut(ctx context.Context, key string) (string, error) {
  out, err := p.p.PresignPutObject(ctx, &s3.PutObjectInput{Bucket: &p.bucket, Key: &key}, s3.WithPresignExpires(2*time.Minute))
  if err != nil {
    return "", err
  }
  return out.URL, nil
}
1 file · go Explain with highlit

When clients upload files directly to S3, your API avoids handling large payloads and you get better scalability. I generate a presigned PUT URL with a short expiry and a constrained object key prefix so users can’t overwrite arbitrary objects. The crucial part is that the backend owns the key: the client asks for an upload, the server returns {key, url}, and the client uploads bytes to S3. Then the client notifies the backend to “finalize” the upload. This two-step flow prevents a class of spoofing bugs where a client claims it uploaded something it didn’t. In production I also include expected content type and size limits in policy (for POST) or enforce them at finalize-time. Presigning is simple, but the workflow around it is where security lives.


Related snips

Share this code

Here's the card — post it anywhere.

Presigned S3 upload URLs (AWS SDK v2) — share card
Link copied