Đừng đẩy toàn bộ byte đi qua server ứng dụng — nó chiếm bộ nhớ/băng thông và giới hạn payload. Dùng direct-to-storage bằng presigned URL.
Luồng presigned (S3):
1. Client gọi API xin quyền upload. Server xác thực + phân quyền, sinh presigned URL (chữ ký tạm, hết hạn sau vài phút, ràng buộc bucket/key/content-type/kích thước).
2. Server trả URL cho client. Client PUT file thẳng lên S3, không đi qua server app.
3. Sau khi lên xong, S3 bắn event (hoặc client báo) → server ghi metadata (owner, key, size) vào DB.
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
const s3 = new S3Client({ region: "ap-southeast-1" })
// after auth + validating type/size, sign a short-lived PUT
const cmd = new PutObjectCommand({ Bucket: "uploads", Key: key, ContentType: type })
const url = await getSignedUrl(s3, cmd, { expiresIn: 300 }) // 5 min
// client then: fetch(url, { method: "PUT", body: file })Ưu: server không gánh byte, scale tốt, tận dụng hạ tầng storage. Nhược: cần cấu hình CORS/quyền cẩn thận, khó chèn xử lý (quét virus, resize) ngay trên luồng → làm bất đồng bộ sau khi upload xong.
Direct-to-storage vs qua server: qua server chỉ hợp file nhỏ hoặc cần biến đổi/kiểm duyệt đồng bộ; file lớn thì presigned + xử lý hậu kỳ. File rất lớn nên kèm multipart/chunked (xem câu resumable). Bẫy: presigned không giới hạn size/type → thành điểm upload không kiểm soát.