Node.js cookbook · Intermediate

Upload files to S3 with presigned URLs from Node.js

Let clients upload large files directly to S3 while the API controls keys, types and sizes.

Approach

Use AWS SDK v3's createPresignedPost to create a short-lived upload form with conditions on content type and size, for a server-generated key under the user's prefix. After upload, the client confirms and the API verifies the object with HeadObject.

Solution

import { randomUUID } from "node:crypto";
import { S3Client, HeadObjectCommand } from "@aws-sdk/client-s3";
import { createPresignedPost } from "@aws-sdk/s3-presigned-post";
import { z } from "zod";

const s3 = new S3Client({ region: process.env.AWS_REGION });
const Bucket = process.env.UPLOAD_BUCKET!;
const UploadRequest = z.object({
  contentType: z.enum(["image/png", "image/jpeg", "application/pdf"]),
  size: z.number().int().positive().max(50 * 1024 * 1024),
});

app.post("/api/uploads", requireAuth, validate({ body: UploadRequest }), async (req, res) => {
  const { contentType, size } = res.locals.body as z.infer<typeof UploadRequest>;
  const Key = `users/${req.user!.id}/${randomUUID()}`;
  const { url, fields } = await createPresignedPost(s3, {
    Bucket, Key,
    Conditions: [["content-length-range", 1, size], ["eq", "$Content-Type", contentType]],
    Fields: { "Content-Type": contentType },
    Expires: 300,
  });
  res.json({ key: Key, url, fields });
});

app.post("/api/uploads/complete", requireAuth, async (req, res) => {
  const key = String(req.body.key ?? "");
  if (!key.startsWith(`users/${req.user!.id}/`)) return res.status(403).json({ title: "Forbidden" });
  const head = await s3.send(new HeadObjectCommand({ Bucket, Key: key }));
  await files.register(req.user!.id, key, head.ContentLength!, head.ContentType!);
  res.status(204).end();
});

Pitfalls

  • Streaming uploads through Node when not needed (memory, bandwidth).
  • Client-chosen keys.
  • Missing CORS configuration on the bucket for browser uploads.

Variations

  • multer + @aws-sdk/lib-storage Upload when the server must process files.
  • S3 event → Lambda for scanning/thumbnails.

Further reading: AWS SDK v3: s3-presigned-post

Reference: https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-s3-presigned-post/

More Node.js recipes