File Uploads End-to-End
Part of the Full Stack Developer Roadmap
In short
Profile photos, resumes, assignment PDFs: uploads touch every layer at once. The browser picks the file, the server checks it, storage keeps it, and the database remembers where it went.
The complete flow of a user selecting a file in React, sending it to your Express backend, and storing it (locally or in cloud storage) — touching the frontend, backend and storage layer all at once.

Think of it like this
The frame shop counter
You hand your photo at the frame shop. The owner checks it fits a size he makes, sends it to the workshop at the back, and gives you a receipt with a token number. Later you collect it with that receipt. Storage is the workshop; the database row is your receipt.
Watch it happen
Uploading an avatar
1/6The user picks photo.png. React puts it into a FormData object.
The big ideas
What you need to know
FormData on the frontend
Append the file to a FormData and send it as the body. The browser adds the multipart headers for you.
Check on the server
Limit the size, allow only the types you expect, and reject the rest. Checks in the browser are just hints.
Rename every file
Generate the stored name with a UUID. User filenames can clash, leak personal info or contain ../ tricks.
Store the pointer, not the file
The database holds the URL or key. The actual bytes live on disk or in cloud storage like S3.
See it in code
Walk through the code
1import multer from "multer";2import { randomUUID } from "node:crypto";3 4const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 2 * 1024 * 1024 } });5const ALLOWED = new Map([["image/png", "png"], ["image/jpeg", "jpg"], ["image/webp", "webp"]]);6 7app.post("/api/avatar", requireUser, upload.single("avatar"), async (req, res) => {8 const ext = req.file && ALLOWED.get(req.file.mimetype);9 if (!req.file || !ext) return res.status(400).json({ error: "PNG, JPEG or WebP only" });10 const key = `avatars/${req.user.id}/${randomUUID()}.${ext}`;11 const url = await storage.save(key, req.file.buffer);12 await pool.query("UPDATE users SET avatar_url = $1 WHERE id = $2", [url, req.user.id]);13 res.status(201).json({ url });14});15 16// Browser: const body = new FormData(); body.append("avatar", file); fetch("/api/avatar", { method: "POST", body });1/5Multer keeps the file in memory and refuses anything over 2 MB.
storage.save is your own small helper for disk or S3. The mimetype comes from the browser, so for stricter checks read the file's signature with a package like file-type.
Save yourself the pain
Common mistakes
Oops
Setting 'Content-Type: multipart/form-data' by hand on the fetch call.
Do this instead
Leave Content-Type out. The browser must add the boundary string, and setting it yourself breaks the upload.
Oops
Saving uploads to the server's own folder on a platform where the disk resets on every deploy.
Do this instead
Use cloud storage like S3 or Cloudinary, or a persistent volume, for anything you need to keep.
Your mission
Ship an avatar upload
Quick check
Where should the actual image bytes of a user's avatar live?
Go deeper
- 1Build a file upload form in React sending a `FormData` request.
- 2Accept and validate the upload on the backend using Multer.
- 3Store the file in cloud storage and return a URL the frontend can display.
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.