typescript 123 lines · 3 tabs

Pre-signed S3 upload from the browser

Shared by codesnips Jan 2026
3 tabs
import { S3Client } from "@aws-sdk/client-s3";
import { createPresignedPost, PresignedPost } from "@aws-sdk/s3-presigned-post";
import { randomUUID } from "crypto";

const s3 = new S3Client({ region: process.env.AWS_REGION });
const BUCKET = process.env.UPLOAD_BUCKET!;

export interface PolicyRequest {
  userId: string;
  contentType: string;
  maxBytes: number;
}

export interface SignedUpload extends PresignedPost {
  key: string;
}

export async function createUploadPolicy(req: PolicyRequest): Promise<SignedUpload> {
  const key = `uploads/${req.userId}/${randomUUID()}`;

  const post = await createPresignedPost(s3, {
    Bucket: BUCKET,
    Key: key,
    Expires: 60,
    Conditions: [
      ["content-length-range", 1, req.maxBytes],
      ["starts-with", "$Content-Type", req.contentType],
      { key },
    ],
    Fields: {
      "Content-Type": req.contentType,
    },
  });

  return { ...post, key };
}
3 files · typescript Explain with highlit

Uploading large files through an application server wastes bandwidth and memory: every byte flows into the API process before being re-uploaded to storage. The pre-signed upload pattern avoids that by having the server sign a short-lived, tightly-scoped policy and letting the browser send the file directly to S3. The server never touches the file body, so it stays cheap and fast even for gigabyte uploads.

In uploadPolicy.ts, createUploadPolicy uses createPresignedPost from @aws-sdk/s3-presigned-post to build an HTML form policy rather than a simple PUT URL. The POST policy is preferred here because it lets the server pin Conditions — an exact key, a Content-Type prefix via starts-with, and a hard content-length-range ceiling — so a leaked policy cannot be reused to upload arbitrary or oversized objects. The key is namespaced under uploads/${userId} and given a randomUUID prefix to prevent collisions and path traversal. Expires keeps the window narrow, and Fields carries values the browser must echo back verbatim.

In uploads.controller.ts, the POST /uploads/presign route validates the requested contentType against an allow-list and rejects anything outside MAX_UPLOAD_BYTES before it signs anything — the policy conditions are a backstop, not the first line of defense. It returns the url and fields to the client and separately hands back the final object key so the frontend can register it after the upload completes.

In useS3Upload.ts, the custom hook orchestrates the two-step flow: it first calls the presign endpoint, then constructs a FormData where every server-provided field is appended before the file (S3 requires file to be the last field). It uses XMLHttpRequest instead of fetch specifically to expose upload.onprogress, driving a real progress bar. A successful direct upload returns 204 No Content, which the hook treats as done before returning the key.

The main trade-offs are that the client must send fields in the correct order and that CORS on the bucket must permit the browser origin. The payoff is that credentials never reach the browser, the server offloads all bandwidth, and every upload is constrained by conditions it cannot forge.


Related snips

ruby
payload = {
  sub: user.id,
  iss: 'https://auth.example.com',
  aud: 'codesnips-api',
  exp: 15.minutes.from_now.to_i,
  iat: Time.now.to_i,

JWT issuance and verification without common footguns

jwt authentication api
by Kai Nakamura 2 tabs
typescript
export type Settled<R> =
  | { status: 'fulfilled'; value: R }
  | { status: 'rejected'; reason: unknown };

export interface ConcurrencyOptions {
  limit: number;

Simple concurrency limiter for batch operations

node concurrency async
by codesnips 2 tabs
bash
#!/usr/bin/env bash
set -euo pipefail

export VAULT_ADDR="https://vault.internal:8200"
export VAULT_TOKEN="${VAULT_TOKEN:?missing VAULT_TOKEN}"

Secrets management with environment isolation and Vault

secrets-management vault environment-variables
by Kai Nakamura 1 tab
typescript
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
  timeout: 15000,

Axios API client with interceptors

react axios api
by Maya Patel 1 tab
typescript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface FilterState {
  search: string
  category: string | null

Zustand for lightweight state management

react zustand state-management
by Maya Patel 2 tabs
typescript
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";

const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";

JWT access + refresh token rotation (conceptual)

security node jwt
by codesnips 3 tabs

Share this code

Here's the card — post it anywhere.

Pre-signed S3 upload from the browser — share card
Link copied