Frontend vs Backend
Tuesday, 11:20: Nadia, who owns a hotel in Lisbon, calls Maya. A three-night suite worth €720 was booked and paid for €7.20, and StayHub's own checkout accepted it.
Every app has two halves: one on the user's phone, one on your servers. Here's what goes where, and why the line between them is about trust.
- Problem
A hotel booking app must feel instant to guests and still charge the right price for a room that really exists.
- Why the simple system fails
Put everything in the browser and anyone can edit the price, skip the availability check or read your secret keys. Put everything on the server and every click waits for a round trip.
- Engineering concept
Split by trust: the frontend owns presentation and interaction; the backend owns rules, data and secrets. Anything the client sends is a claim to verify, never a fact.
- Technology
HTML/CSS/JS frameworks (React, Next.js) on the client; an API server (Node, Django, Go…) with a database behind it.
- Trade-offs
Logic on the client is fast and cheap to run but untrustworthy. Logic on the server is trustworthy but costs latency and servers. Validation usually lives in both, for different reasons.
The problem
2 min- NadiaMy sea-view suite is €240 a night. Someone booked 3 nights and paid €7.20. Your system said it was fine.
- Maya · Product managerWhich part of StayHub decided that price: the guest's phone, or our servers?
- Lina · Senior engineer, your mentorGuess before we look.
The big idea
4 minThe frontend draws and asks. The backend checks and decides. The line between them is about trust, not languages.
The guest owns the laptop and can change anything it sends. So our server must never believe a number it can compute itself, like a price.
How it works
5 minThe is the part that runs on the user's device and draws screens. The runs on our servers: it applies the rules, keeps the data, holds the secrets. They talk through an . Follow one booking:
- 1Download the frontend
The browser gets StayHub's JavaScript from the CDN. From now on it runs on the guest's laptop, out of our hands.
- 2Help the guest
The page shows the calendar, greys out past dates and displays an estimated total. Fast, friendly, and editable by the guest.
- 3Cross the trust boundary
The page sends POST /bookings. The trust boundary is the line between machines we control and machines we don't: everything crossing it inwards could have been changed.
- 4The backend decides
Our server checks who is asking, loads the real rate and checks the room is free. It reads only intent (room, dates), never claims it can compute (the price).
- 5Charge with a secret
The server charges the card using a secret key the browser never sees.
- 6Show the answer
The response comes back and the page draws the confirmation, with the server's total.
Validation lives in both places for different reasons: in the browser for instant feedback, on the server because it's the real check. Permissions live only on the server.
Watch a decision being made
4 minLina closes the €7.20 hole
Booking #4127 paid €7.20 for €720 of nights. Make sure no guest can choose their own price again, without breaking checkout for honest guests.
- 1Reproduce it
Send the same request with
totalPrice: 1to a test copy of StayHub. The server answers201 Created.Why: If you can't make it happen on purpose, you can't prove the fix works.
- 2Sort each field: intent or claim
roomId,checkIn,checkOutare intent.totalPriceis a claim the server can compute. The user id comes from the session, never the body.Why: Claims can be computed, so accepting them only adds risk.
- 3Move the decision to the server
Stop reading the price from the body. Load the rate, compute the total, then charge it.
tsapp.post("/bookings", requireAuth, async (req, res) => { const { roomId, checkIn, checkOut } = BookingInput.parse(req.body); // no price const room = await db.rooms.findById(roomId); if (!room) return res.status(404).end(); const total = priceFor(room, checkIn, checkOut); // 3 × €240 = €720 await payments.charge(req.user.id, total); res.status(201).json({ total }); });Why: The server owns the rates, so it's the only place the price can be trusted.
- 4Attack it again
Resend the edited request: the server charges €720. Then check past bookings where paid differs from rate × nights.
Why: A security fix you haven't attacked is a hope, and the hole may have been used before #4127.
In real life
3 minFigma: a thick client
- Has publicly described running its editor's rendering engine in the browser (C++ compiled to WebAssembly)
- Dragging and zooming must react in milliseconds
- Saving files and sharing permissions still go through its servers
Worth it for interactive tools where every millisecond of feedback matters.
Wikipedia: a thin client
- Pages are built as HTML on its servers and heavily cached
- The browser mostly displays text and links
- Editing rules are all server-side
Worth it when content matters more than interaction.
What goes wrong
3 minThe endpoint behind it is still reachable.
Check the caller's role on the server on every request.
A script calling the API directly skips browser checks entirely.
Always validate on the server; the browser check is only for feedback.
Everything shipped to the browser is public, including NEXT_PUBLIC_… variables.
Keep secrets on the server; route calls that need them through the backend.
Trade-offs
3 min| Option | Fits when | You pay | Main risk |
|---|---|---|---|
| Thick client | Interactive tools (editors, maps) | Logic copied across web and apps | Trusting what it computes |
| Thin client | Content, admin tools, checkout | A round trip per interaction | A sluggish UI |
| Validation in both | Almost always, for user input | Two copies of simple checks | They drift apart |
- Karim · CTOCopy the price formula into the browser so the total updates instantly as guests change dates.
- Lina · Senior engineer, your mentorThen we have two formulas. The day one gets a weekend rate, guests see €690 and pay €720.
- Karim · CTOYour way, a guest in Tokyo waits 230 ms every time they touch the calendar.
Your job as tech lead
4 minClaude Code can write the code for this. You still own the decisions, and you have to know what good looks like to accept or reject its work.
- Which values the server computes itself and never accepts from the browser: price, total, discount, the guest's role.
- Which secrets exist (payment key, database password); none may land in the frontend bundle or a
NEXT_PUBLIC_variable. - Who may call each endpoint, like
POST /refunds: decided per role on the server, not by hiding a button.
Booking #4127 was charged €7.20 instead of €720 because our checkout sends totalPrice and the server trusts it. Fix POST /bookings: ignore any price from the browser and compute the total from the room's rate in PostgreSQL. The confirmation shows the server's number. Don't change payment code or the schema. Show your plan first, listing every money field the client sends. Add a test that sends totalPrice 7.20 and proves €720 is charged.
- Claude lists every field the browser sends; only intent (room, dates) is trusted.
- The test sends a tampered price and asserts €720 was charged.
- A normal guest calling the refund endpoint directly gets 403.
- It "fixes" tampering by obfuscating the JavaScript or adding a check only in React.
- It hides a button and calls the feature secure, with no role check in the handler.
- It moves the payment secret into a frontend variable "so checkout works".
Check yourself
2 min{ roomId, dates, totalPrice } to POST /bookings. A security researcher books a €900 suite for €1. What is the correct fix?Next episode: Friday's flash sale sends five times the usual traffic to our one server in Frankfurt, and Karim asks what "the server" actually is.
Mark it complete to update your skill map.
Go deeper (optional)You don't need this to understand the lesson. Open it when a project needs the details: Technical deep dive, Interactive simulation, Visual diagram, Analogy, Check your understanding, Practice ladder, Explain it yourself.
Technical deep dive
Here's the request that booked the suite for €7.20. An attacker doesn't even need our page: any HTTP tool can send it.
POST /bookings HTTP/1.1
Host: api.stayhub.com
Cookie: session=7f3a…
Content-Type: application/json
{ "roomId": 12, "checkIn": "2026-10-09", "checkOut": "2026-10-12", "totalPrice": 7.2 }Lina closes the €7.20 hole
Booking #4127 paid €7.20 for €720 of nights. Make sure no guest can choose their own price again, without breaking the checkout for honest guests.
- 1Reproduce it
Send the same request with
totalPrice: 1against a test copy of StayHub and confirm the server answers201 Created.Why: If you can't make it happen on purpose, you can't prove the fix works.
- 2Draw the trust boundary
Draw a dashed line between the guest's browser and our server. Every arrow crossing it inwards carries data the guest could have changed.
Why: It turns a vague worry ("is this safe?") into a concrete list of inputs to check.
- 3Sort each field: intent or claim
roomId,checkIn,checkOutare intent: what the guest wants.totalPriceis a claim: something the server can compute itself. The user id comes from the session, never from the body.Why: Intent must come from the guest. Claims can be computed, so accepting them only adds risk.
- 4Move the decision to the server
Stop reading the price from the body. Validate the dates, load the rate from the database, compute the total, check availability, then charge that amount.
tsapp.post("/bookings", requireAuth, async (req, res) => { const input = BookingInput.parse(req.body); // room + dates only if (input.checkOut <= input.checkIn) return res.status(400).json({ error: "invalid_dates" }); const room = await db.rooms.findById(input.roomId); if (!room) return res.status(404).end(); const total = priceFor(room, input.checkIn, input.checkOut); // 3 × €240 = €720 await payments.charge(req.user.id, total); res.status(201).json({ bookingId: "…", total }); });Why: The server owns the rates in the database, so it's the only place the price can be trusted.
- 5Keep the frontend helpful
The page can still display an estimate while the guest picks dates, but the confirmation screen shows the server's total from the response.
Why: Honest guests keep instant feedback; the number that's charged has a single source.
- 6Attack it again
Resend the edited request. The server ignores
totalPriceand charges €720. Then check past bookings where the paid amount differs from rate × nights.Why: A security fix you haven't attacked is a hope, and the same hole may already have been used before #4127.
| Responsibility | Frontend | Backend |
|---|---|---|
| Drawing the UI, animations, layout | Owns it | Can pre-render HTML () |
| Input validation | For UX: instant feedback | For correctness and security: always |
| Price, discounts, taxes | Displays them | Computes and enforces them |
| Who can do what | Hides buttons the user can't use | Rejects the request (403) |
| Secrets (payment keys, DB password) | Never | Environment variables or a |
| Data storage | Temporary (memory, localStorage) | Durable source of truth (database) |
Interactive simulation
Visual diagram
A hotel booking from click to confirmation. Start the walkthrough to see who decides what.
Analogy
- At a hotelThe brochure and booking formIn an appThe frontend on the user's device
- At a hotelA guest scribbling on the formIn an appA user editing requests in dev tools
- At a hotelHanding the form to receptionIn an appSending a request through the API
- At a hotelReception checking the real rateIn an appThe backend enforcing the rules
- At a hotelThe reservation bookIn an appThe database
Where the analogy breaks: a guest rarely forges a form. Online, anyone can send thousands of fake requests per second with a short script, so the backend must check every single one.
Check your understanding
Practice ladder
Trace a coupon request
Scenario Warm-upStayHub's checkout now has coupons. Here's the handler:
app.post("/bookings", requireAuth, async (req, res) => {
const { roomId, checkIn, checkOut, couponPercent } = req.body;
const room = await db.rooms.findById(roomId);
const total = priceFor(room, checkIn, checkOut) * (1 - couponPercent / 100);
await payments.charge(req.user.id, total);
res.status(201).json({ total });
});The browser checks the coupon code against a list and, if it's valid, sends couponPercent: 10.
A guest books Nadia's suite (€240/night) for 3 nights and edits the request to send couponPercent: 100. Trace what the server does, step by step, and say what gets charged.
Finish Lina's fix for the booking page
Code PracticeGuests can view a booking at GET /bookings/:id. Omar found that changing the URL from /bookings/1043 to /bookings/1044 shows another guest's booking. Lina started the fix:
- Reproduce it: signed in as guest A, request a booking that belongs to guest B. It's returned.
- Draw the trust boundary: the id in the URL comes from the browser.
- Sort the inputs: ___
- Move the decision to the server: ___
- Attack it again: request B's booking as A; expect 404.
Fill in steps 3 and 4. For step 4, write the query or the check.
Nadia edits her room rates
Design PracticeStayHub is building a page where hotel owners set their nightly rates. The first draft:
- The owner dashboard lists only the owner's own hotels.
- Saving sends
PUT /hotels/:hotelId/rateswith{ roomId, nightlyRate }. - The form refuses rates below €10 or above €5,000.
- The server saves whatever it receives, as long as the caller is signed in.
Nadia owns hotel 7. List everything a malicious signed-in user could do with this design, and describe what the server must check before saving.
The map key: browser or server?
Decision StretchMaya wants a map on every hotel page. The map provider gives StayHub two keys:
- A browser key, designed to be public, that only works on pages from
stayhub.comand only loads map tiles. Usage is billed per tile. - A secret server key that can also change billing settings and read usage reports.
Karim: "Put the browser key in the frontend, it's made for that." Omar: "Nothing with a key goes in the browser. Proxy every map tile through our server."
Our one server in Frankfurt handles about 5,000 guests a day. Each hotel page loads about 20 map tiles.
Write a short decision: "We chose X over Y because…, accepting the risk of…"
Explain it yourself
Explain to a new teammate why the booking price must be computed on the backend, even though the frontend already shows it.