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-storageUploadwhen 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
- Add health and readiness endpoints to a Node.js service
- Add JWT authentication to an Express API
- Cache expensive lookups in Redis from Node.js
- Call external APIs with timeouts and retries in Node.js
- Centralize error handling in Express 5
- Hash passwords and implement login safely in Node.js
- Implement cursor pagination with PostgreSQL in Node.js
- Offload CPU-heavy work with worker threads