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 };
}
import { Router, Request, Response } from "express";
import { createUploadPolicy } from "./uploadPolicy";
const router = Router();
const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp", "application/pdf"];
const MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
router.post("/uploads/presign", async (req: Request, res: Response) => {
const { contentType, size } = req.body as { contentType?: string; size?: number };
const userId = req.user?.id;
if (!userId) {
return res.status(401).json({ error: "authentication required" });
}
if (!contentType || !ALLOWED_TYPES.includes(contentType)) {
return res.status(400).json({ error: "unsupported content type" });
}
if (typeof size !== "number" || size <= 0 || size > MAX_UPLOAD_BYTES) {
return res.status(400).json({ error: "file too large" });
}
const signed = await createUploadPolicy({
userId,
contentType,
maxBytes: MAX_UPLOAD_BYTES,
});
return res.json({
url: signed.url,
fields: signed.fields,
key: signed.key,
});
});
export default router;
import { useCallback, useState } from "react";
interface PresignResponse {
url: string;
fields: Record<string, string>;
key: string;
}
export function useS3Upload() {
const [progress, setProgress] = useState(0);
const [error, setError] = useState<string | null>(null);
const upload = useCallback(async (file: File): Promise<string> => {
setError(null);
setProgress(0);
const presignRes = await fetch("/uploads/presign", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ contentType: file.type, size: file.size }),
});
if (!presignRes.ok) throw new Error("failed to obtain upload policy");
const { url, fields, key }: PresignResponse = await presignRes.json();
const form = new FormData();
Object.entries(fields).forEach(([name, value]) => form.append(name, value));
form.append("file", file); // must be the last field for S3
await new Promise<void>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("POST", url);
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) setProgress(Math.round((e.loaded / e.total) * 100));
};
xhr.onload = () => {
if (xhr.status === 204) resolve();
else reject(new Error(`S3 rejected upload: ${xhr.status}`));
};
xhr.onerror = () => reject(new Error("network error during upload"));
xhr.send(form);
}).catch((e: Error) => {
setError(e.message);
throw e;
});
return key;
}, []);
return { upload, progress, error };
}
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
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
export type Settled<R> =
| { status: 'fulfilled'; value: R }
| { status: 'rejected'; reason: unknown };
export interface ConcurrencyOptions {
limit: number;
Simple concurrency limiter for batch operations
#!/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
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
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface FilterState {
search: string
category: string | null
Zustand for lightweight state management
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)
Share this code
Here's the card — post it anywhere.