Stop 09 · Connecting Frontend & BackendTopics · 8/8

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.

The blob character hands a mountain photo across a frame shop counter with empty frames on the wall.

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

Browser
Express API
Storage
Postgres
<input type=file>

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

ts
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

~40 min

Quick check

Where should the actual image bytes of a user's avatar live?

Go deeper

  1. 1Build a file upload form in React sending a `FormData` request.
  2. 2Accept and validate the upload on the backend using Multer.
  3. 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.

Download
InsideEdge