Skip to content
Episode 2 · The €7.20 suite

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.

30 min foundationWhat the frontend is responsible forWhat the backend is responsible forNever trust the clientClient vs serverRequest / response cycleAuthentication (who you are) vs authorization (what you may do)
  1. Problem

    A hotel booking app must feel instant to guests and still charge the right price for a room that really exists.

  2. 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.

  3. 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.

  4. Technology

    HTML/CSS/JS frameworks (React, Next.js) on the client; an API server (Node, Django, Go…) with a database behind it.

  5. 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.

01

The problem

2 min
Tuesday, 11:20. Maya puts Nadia, a hotel owner in Lisbon, on speaker.
  1. Nadia
    My sea-view suite is €240 a night. Someone booked 3 nights and paid €7.20. Your system said it was fine.
  2. Maya · Product manager
    Which part of StayHub decided that price: the guest's phone, or our servers?
  3. Lina · Senior engineer, your mentor
    Guess before we look.
02

The big idea

4 min
Who decided the price?
The big idea

The 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.

03

How it works

5 min

The 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:

One booking, two computers
  1. 1
    Download 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.

  2. 2
    Help the guest

    The page shows the calendar, greys out past dates and displays an estimated total. Fast, friendly, and editable by the guest.

  3. 3
    Cross 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.

  4. 4
    The 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).

  5. 5
    Charge with a secret

    The server charges the card using a secret key the browser never sees.

  6. 6
    Show 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.

04

Watch a decision being made

4 min
Worked example

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 checkout for honest guests.

  1. 1Reproduce it

    Send the same request with totalPrice: 1 to a test copy of StayHub. The server answers 201 Created.

    Why: If you can't make it happen on purpose, you can't prove the fix works.

  2. 2Sort each field: intent or claim

    roomId, checkIn, checkOut are intent. totalPrice is 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.

  3. 3Move the decision to the server

    Stop reading the price from the body. Load the rate, compute the total, then charge it.

    ts
    app.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.

  4. 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.

The whole thing in one line: Keep only the guest's intent, compute everything that matters on the server. The page may still display an estimate; it's shown, never trusted.
05

In real life

3 min
Two well-known products, two different splits

Figma: 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.

06

What goes wrong

3 min
Hiding a button as access control

The endpoint behind it is still reachable.

Instead

Check the caller's role on the server on every request.

Validating only in the frontend

A script calling the API directly skips browser checks entirely.

Instead

Always validate on the server; the browser check is only for feedback.

Putting secret keys in frontend code

Everything shipped to the browser is public, including NEXT_PUBLIC_… variables.

Instead

Keep secrets on the server; route calls that need them through the backend.

07

Trade-offs

3 min
OptionFits whenYou payMain risk
Thick clientInteractive tools (editors, maps)Logic copied across web and appsTrusting what it computes
Thin clientContent, admin tools, checkoutA round trip per interactionA sluggish UI
Validation in bothAlmost always, for user inputTwo copies of simple checksThey drift apart
Wednesday, 15:00. The fix is live.
  1. Karim · CTO
    Copy the price formula into the browser so the total updates instantly as guests change dates.
  2. Lina · Senior engineer, your mentor
    Then we have two formulas. The day one gets a weekend rate, guests see €690 and pay €720.
  3. Karim · CTO
    Your way, a guest in Tokyo waits 230 ms every time they touch the calendar.
08

Your job as tech lead

4 min

Claude 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.

Decisions you own
  • 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.
Tell Claude Code
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.
Check before you accept
  • 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.
Red flags
  • 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".
09

Check yourself

2 min
Q1/3Scenario
Your checkout sends { roomId, dates, totalPrice } to POST /bookings. A security researcher books a €900 suite for €1. What is the correct fix?
How confident are you?

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.

Finished?

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.
10

Technical deep dive

Move the decision to the server

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.

http
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 }
Editing a request takes seconds with browser dev tools or curl.
Worked example

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.

  1. 1Reproduce it

    Send the same request with totalPrice: 1 against a test copy of StayHub and confirm the server answers 201 Created.

    Why: If you can't make it happen on purpose, you can't prove the fix works.

  2. 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.

  3. 3Sort each field: intent or claim

    roomId, checkIn, checkOut are intent: what the guest wants. totalPrice is 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.

  4. 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.

    ts
    app.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.

  5. 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.

  6. 6Attack it again

    Resend the edited request. The server ignores totalPrice and 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.

The whole thing in one line: Lina reproduced the bug, drew the line between the guest's machine and ours, kept only the guest's intent (room and dates), and made the server compute and charge the price itself.
ResponsibilityFrontendBackend
Drawing the UI, animations, layoutOwns itCan pre-render HTML ()
Input validationFor UX: instant feedbackFor correctness and security: always
Price, discounts, taxesDisplays themComputes and enforces them
Who can do whatHides buttons the user can't useRejects the request (403)
Secrets (payment keys, DB password)NeverEnvironment variables or a
Data storageTemporary (memory, localStorage)Durable source of truth (database)
A useful test for each row: what if the user controlled this code?
11

Interactive simulation

12

Visual diagram

Click a component

A hotel booking from click to confirmation. Start the walkthrough to see who decides what.

13

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.

14

Check your understanding

Q1/3Decision
Match each responsibility to where its authoritative version should live:
Greying out past dates in the calendar
Checking the room is still free
Payment provider secret key
Formatting €1240 as “€1,240.00”
How confident are you?
15

Practice ladder

Step 1 · Trace

Trace a coupon request

Scenario Warm-up

StayHub'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.

Step 2 · Complete

Finish Lina's fix for the booking page

Code Practice

Guests 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:

  1. Reproduce it: signed in as guest A, request a booking that belongs to guest B. It's returned.
  2. Draw the trust boundary: the id in the URL comes from the browser.
  3. Sort the inputs: ___
  4. Move the decision to the server: ___
  5. 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.

Step 3 · Solve

Nadia edits her room rates

Design Practice

StayHub 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/rates with { 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.

Step 4 · Decide

The map key: browser or server?

Decision Stretch

Maya 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.com and 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…"

16

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.

0 words