File Storage Service (S3-like)
Part of the Backend Developer Roadmap
In short
Profile photos, PDFs, resumes: every app stores files. Building a mini S3 teaches you to keep big bytes out of your API servers and still control exactly who can read what.
Your own simplified object storage service — practice for file handling, streaming, and the kind of infrastructure-adjacent thinking storage systems require.

Think of it like this
The bank locker room
The bank keeps a register: locker 214 belongs to Mrs Sharma. The valuables sit in the steel locker, not in the register. To open it you need the bank's permission and a key. Metadata in the register, bytes in the locker.
Watch it happen
Uploading with a presigned URL
POST /files
1/7The browser asks to upload resume.pdf, 2 MB, as application/pdf.
The big ideas
What you need to know
Metadata vs bytes
Owner, name, size, type and status go in your database. The actual bytes go in object storage.
Presigned URLs
Short-lived signed links let clients upload and download directly, while you still decide who gets one.
Private by default
Buckets stay private. Every download goes through a permission check that hands out a fresh signed link.
Big files in parts
Multipart uploads send large files in chunks, so a dropped connection only retries one part.
See it in code
Walk through the code
1import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";2import { getSignedUrl } from "@aws-sdk/s3-request-presigner";3import { randomUUID } from "node:crypto";4 5const s3 = new S3Client({ region: "ap-south-1", endpoint: process.env.S3_ENDPOINT, forcePathStyle: true });6 7app.post("/files", requireAuth, async (req, res) => {8 const key = `users/${req.user.id}/${randomUUID()}`; // never use the client's filename as the key9 const uploadUrl = await getSignedUrl(10 s3,11 new PutObjectCommand({ Bucket: "uploads", Key: key, ContentType: req.body.contentType }),12 { expiresIn: 300 }, // valid for 5 minutes13 );14 await db.file.create({ data: { key, ownerId: req.user.id, status: "pending" } });15 res.status(201).json({ key, uploadUrl });16});1/4An S3 client. With a custom endpoint it talks to MinIO or any S3-compatible store.
The same code works against AWS S3 or a local MinIO container: just change S3_ENDPOINT. Validate size and content type before signing.
Save yourself the pain
Common mistakes
Oops
Streaming every upload through your Node.js API server.
Do this instead
Big uploads tie up your server and memory. Let clients upload straight to storage with presigned URLs.
Oops
Making the bucket public so images load easily.
Do this instead
Public buckets leak private files. Keep it private and serve signed GET URLs or go through a CDN with signed access.
Your mission
Milestone 1: upload and download
Quick check
What's the main benefit of presigned upload URLs?
Go deeper
- 1Support upload, download and delete operations for files of varying sizes.
- 2Stream large files instead of loading them fully into memory.
- 3Add access control so files can be private, public, or shared via signed URLs.
More projects 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.