In short
Never keep user uploads on your server's disk. S3 stores any number of files, keeps redundant copies, and serves them over HTTPS while your app servers stay stateless.
Amazon's object storage service — the standard place to store files (uploads, backups, static assets) at massive scale with high durability, outside your application server.

Think of it like this
The bank locker room
A bank gives you a locker and a key. You don't care which wall it's in or how thick the steel is. You put things in, take things out, and the bank handles guards and backups. Buckets are your lockers, objects are what's inside.
Watch it happen
Uploading straight to S3
POST /uploads/avatar
1/5The browser asks your API for permission to upload a profile photo.
The big ideas
What you need to know
Buckets and keys
A bucket holds objects. Each object has a key like invoices/2026/42.pdf. The slashes only look like folders.
Private by default
New buckets block public access. Share files with presigned URLs or a CDN, not by making the bucket public.
Presigned URLs
A time-limited signed link that lets someone upload or download one object without having AWS credentials.
Storage classes
Rarely read files can move to cheaper classes like Glacier using lifecycle rules. Retrieval gets slower and costs more.
See it in code
Walk through the code
1import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";2import { getSignedUrl } from "@aws-sdk/s3-request-presigner";3 4const s3 = new S3Client({ region: "ap-south-1" });5 6app.post("/uploads/avatar", requireAuth, async (req, res) => {7 const key = `avatars/${req.user.id}.jpg`;8 const command = new PutObjectCommand({9 Bucket: process.env.BUCKET!,10 Key: key,11 ContentType: "image/jpeg",12 });13 const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 300 });14 res.json({ uploadUrl, key });15});1/4ap-south-1 is the Mumbai region, close to Indian users.
The browser then sends PUT to uploadUrl with the same Content-Type header. The bucket also needs a CORS rule allowing your site's origin.
Save yourself the pain
Common mistakes
Oops
Making the whole bucket public so images load.
Do this instead
Keep it private and serve through CloudFront or presigned GET URLs. Public buckets are a classic data leak.
Oops
Streaming every upload through your Express server.
Do this instead
Big files then eat your server's memory and bandwidth. Let the browser upload directly with a presigned URL.
Your mission
Direct-to-S3 uploads
Quick check
Why does the API hand out a presigned URL instead of receiving the file itself?
Go deeper
- 1Upload and retrieve a file from an S3 bucket using the AWS SDK.
- 2Learn how to generate a signed URL for temporary, secure access to a private file.
- 3Configure a bucket for public read access for use as a simple CDN origin.
More topics at this stop
Stuck on this topic? Ask an Insider
Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.