Part 01 · The Backend Mindset

Client vs Server

ClientServer

Who is responsible for what, and where the boundary between a browser and a backend really sits.

⏱ 1 hours · Station 2 of 153

The price was 500. Someone paid 1.

You run a small online bookshop. A book costs 500 taka. One morning you check the orders and find a customer who bought two books and paid 2 taka.

Nobody hacked your server. Nobody guessed a password. The app simply sent the price along with the order, qty=2&price=500, and the server multiplied what it was given. This customer sent price=1 instead.

How is that even possible, when your app never shows a box to type a price? And whose fault is it: the app's, or the server's?

This lesson answers both questions by drawing one line through every system you will ever build: the line between the client and the server.

The problem: two programs, two owners

When software serves many users (lesson 1), it splits into two programs that run in very different places.

ClientServer
Where it runsOn the user's phone, laptop or browserOn a machine you control
Who controls itThe user. They can inspect it, change it, or replace it entirelyYou, and only you
How many copiesOne per user, millions of them, many out of dateOne, or a few identical ones
What it's good atShowing things, reacting instantly to taps and keysKeeping the shared data, enforcing the rules, keeping secrets
Can it be trusted?NoYes, if you build it carefully

The row that surprises people is "who controls it". Your app is installed on a stranger's device. Once it's there, it belongs to them. They can read its code, watch every request it sends, and write their own requests from scratch with a tool like curl, skipping your app completely.

So the server never really talks to your app. It talks to whatever program sends it bytes.

Think of a restaurant

In a restaurant, you sit at a table with a menu. You choose, you point, you ask questions. That's the client: it's where you interact, and it's yours to use however you like.

The kitchen is the server. It holds the ingredients (the data), the recipes (the rules) and the cash register (the money). You don't walk into the kitchen. You send in an order, and the kitchen decides what actually happens.

Now imagine the order slip had a line where you write the price. Most customers would copy the menu honestly. One would write "1 taka". If the kitchen charges whatever the slip says, that's not the customer's cleverness, it's the restaurant's design mistake.

client: your deviceserver: your machinetrust boundaryshop appshows, collects tapscurlanyone can write a requestserverbook = 500 takaqty=2charged 1000qty=2&price=1price ignored: still 1000the server decides
The app sends only what the user chose. A hand-written request with a fake price crosses the same wire, and the server ignores the price because it never asked the client for one.

Where the analogy stops working. A waiter can look at a customer and notice something odd. A server can't see anyone: every request is just text, and a request typed by an attacker looks exactly like one sent by your app. There is no "face" to check. That's why the rule has to be built into the server itself.

The building blocks

Strip any app down and you find the same few pieces.

  1. Request. The client sends a message: "here is what the user chose." In practice that's an HTTP request: a method, a path, some headers and maybe a body. (Module 3.)
  2. Response. The server answers: "here is what happened." A status code, headers and a body.
  3. The boundary. Everything on the client side is input: useful, but unverified. Everything on the server side is decision: what the price is, whether you may do this, what gets saved.
  4. Responsibilities. Each side has a job it's best at:
Belongs on the clientBelongs on the server
Drawing the screen, layout, animationsPrices, totals, discounts, stock
Instant feedback ("email looks wrong")The real check, every time
Remembering the open tab or a draftData shared between users
Hiding a button the user can't useRefusing the action when they try anyway
Nothing secretAPI keys, passwords, other people's data
  1. The golden rule. Client checks are for convenience; server checks are for correctness. A form can say "quantity must be 1 to 10" so honest users get quick feedback. The server must check the same thing again, because the form can be skipped.
Predict first

Your checkout form uses JavaScript to stop quantities above 10. A user opens the browser's developer tools and sends qty=500 directly. What decides whether that order goes through?

Behind the scenes: one order, two paths

Here is the same order arriving twice: once from your app, once typed by hand.

Step through it

The same order, sent two ways

Shop appcurlServerPrice list

Step 1/6 The user taps "Buy 2"

The app builds a request from what the user chose. A careful app sends only the choice: which book and how many. No price.

request
GET /checkout?item=book&qty=2

The picture to keep in your head

Draw a vertical line between your users and your server. Label it trust boundary.

  • Left of the line: thousands of copies of your app, on devices you don't control, some modified, some old, some not your app at all.
  • Right of the line: one program you control, with the data and the rules.
  • Everything crossing the line from left to right is a claim: "the user wants this". The server checks every claim, every time.

Whenever you add a feature, ask: if someone skipped my app and sent this request by hand, what's the worst they could do? If the answer is "change a price", "read someone else's data" or "skip a payment", that logic is on the wrong side of the line.

Try it: which side should decide?

Each card is a piece of logic from a real app. Decide where it belongs before you read the answer.

Try each case

Client, server, or both?

client: instant feedback while typing
server: checks the email again on submit
works: Both. The client check makes the form friendly; the server check is the one that counts. Without it, a request from curl could save garbage.

Code from first principles

A tiny shop server with two endpoints, built only from the standard library:

  • /trust multiplies whatever quantity and price the client sent.
  • /checkout reads only the item and the quantity, looks up the price itself, and checks the quantity.

Then three requests are written by hand, as raw text over a TCP socket, exactly the way curl or an attacker's script would. The first is what your app sends. The other two are not.

package main

import (
	"bufio"
	"fmt"
	"net"
	"net/http"
	"strconv"
	"strings"
)

// The price lives on the server. The client never gets to decide it.
var prices = map[string]int{"book": 500}

// /trust believes whatever the client sent, price included.
func trust(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	qty, _ := strconv.Atoi(q.Get("qty"))
	price, _ := strconv.Atoi(q.Get("price"))
	fmt.Fprintf(w, "charged %d", qty*price)
}

// /checkout takes only what the user chose, and decides everything else itself.
func checkout(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	price, ok := prices[q.Get("item")]
	if !ok {
		http.Error(w, "no such item", http.StatusNotFound)
		return
	}
	qty, err := strconv.Atoi(q.Get("qty"))
	if err != nil || qty < 1 || qty > 10 {
		w.WriteHeader(http.StatusBadRequest)
		fmt.Fprint(w, "qty must be 1 to 10")
		return
	}
	fmt.Fprintf(w, "charged %d", qty*price)
}

// send writes an HTTP request by hand, the way any program could, and returns "status body".
func send(addr, path string) string {
	conn, err := net.Dial("tcp", addr)
	if err != nil {
		panic(err)
	}
	defer conn.Close()
	fmt.Fprintf(conn, "GET %s HTTP/1.1\r\nHost: shop\r\nConnection: close\r\n\r\n", path)
	reply := bufio.NewReader(conn)
	status, _ := reply.ReadString('\n') // "HTTP/1.1 200 OK"
	for {                               // skip the headers: they end at the first empty line
		line, err := reply.ReadString('\n')
		if line == "\r\n" || err != nil {
			break
		}
	}
	body, _ := reply.ReadString(0)
	return strings.Fields(status)[1] + " " + strings.TrimSpace(body)
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/trust", trust)
	mux.HandleFunc("/checkout", checkout)
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		panic(err)
	}
	go http.Serve(ln, mux)
	addr := ln.Addr().String()

	fmt.Println("a book costs 500 taka; that price is stored on the server")
	fmt.Printf("%-6s %-22s %-20s %s\n", "sender", "asks for", "/trust", "/checkout")
	for _, c := range [][2]string{
		{"app", "item=book&qty=2&price=500"},
		{"curl", "item=book&qty=2&price=1"},
		{"curl", "item=book&qty=-3&price=500"},
	} {
		fmt.Printf("%-6s %-22s %-20s %s\n", c[0], strings.TrimPrefix(c[1], "item=book&"), send(addr, "/trust?"+c[1]), send(addr, "/checkout?"+c[1]))
	}
}

Output (the same in all five languages):

a book costs 500 taka; that price is stored on the server
sender asks for               /trust               /checkout
app    qty=2&price=500        200 charged 1000     200 charged 1000
curl   qty=2&price=1          200 charged 2        200 charged 1000
curl   qty=-3&price=500       200 charged -1500    400 qty must be 1 to 10

Reading the output:

  • app, qty=2&price=500: both endpoints charge 1000. With an honest client, you can't tell the two designs apart. That's what makes this bug so easy to ship.
  • curl, price=1: /trust charges 2 taka. /checkout charges 1000, because it never read the price.
  • curl, qty=-3: /trust charges −1500, which in a real payment flow could be a refund. /checkout answers 400: the quantity rule lives on the server, so it runs no matter who sent the request.
  • Notice that the server never needed to know who sent each request. It stayed safe by only accepting choices from the client and computing everything else itself.

Real engineering

  • Never trust the client. This is the first rule of backend security, and it covers prices, user ids, roles, file names, and sizes. Validate on the server even when the client validates too. (Module 5 and Module 9.)
  • Send ids, not facts. A good client sends item=book or productId=42; the server looks up the name, price and stock. A client that sends price or isAdmin=true is asking to be lied to.
  • Thin clients, thick server, for anything that matters. Teams put money, permissions and shared data on the server, and keep the client fast and friendly. Some logic is duplicated on purpose: the client checks for speed, the server checks for truth.
  • Old clients live forever. Users don't update apps. Your server must keep answering requests from versions you shipped years ago, which is why APIs are versioned. (Module 3, lesson 15.)
  • Mobile apps are not secret. Anything shipped in an app (keys, hidden URLs, "admin" flags) can be extracted. Real systems keep secrets on the server and give clients short-lived tokens instead.

When it breaks

Every one of these has happened in real products.

Try each case

Pick a failure

POST /orders {"item": 42, "price": 1}
order saved, charged 1 taka
breaks: The server trusted a fact it should have looked up. Fix: send only the product id; compute the price on the server. Diagnose by comparing charged amounts against the price list.

Think before you read on

Your app is only used through your own mobile app, and the API URL isn't written anywhere public. Is it safe to skip server-side checks for now?

Reveal

No. The API URL is inside the app, and anyone can find it by watching the app's network traffic, which takes about five minutes with a free proxy tool. "Nobody knows the URL" is not a protection. The server must assume every request could have been written by hand, because some of them will be.

Check yourself

1. In one sentence, what is the difference between a client and a server?

Answer

The client runs on the user's device and is controlled by the user; the server runs on a machine you control and holds the shared data and the rules.

2. Why should a client send productId instead of price?

Answer

Because anything the client sends can be changed. An id is just a choice; the server looks up the real price from its own data, so a changed id can only select a different product, not a different price.

3. If the server checks everything anyway, why validate in the client at all?

Answer

For the user. Instant feedback is faster and friendlier than a round trip, and it saves the server some useless requests. It just isn't security.

4. Name two things that must never be stored in a client.

Answer

For example: secret API keys, passwords or other users' data, and the authority to decide prices or permissions.

Your challenge: close the hole

Add a third endpoint, /discount?item=book&qty=2&coupon=HALF, to the program above.

  • The server has its own list of coupons: HALF takes 50% off, TEN takes 10% off.
  • An unknown coupon is answered with 400 unknown coupon.
  • The client must not be able to send a discount amount, only a coupon code.

Before you run it, predict what /discount?item=book&qty=2&coupon=HALF&discount=100 should return.

You've succeeded when your hand-written requests print 200 charged 500 for HALF, 200 charged 900 for TEN, 400 unknown coupon for FREE, and the extra discount=100 changes nothing.

The mental model

The client is the user's program, on the user's device, under the user's control; the server is yours. Everything that crosses the line is a claim, not a fact. Let the client show and suggest; let the server decide, compute and remember. Check on the client for speed, and on the server for truth.

Station stamp

Finish the chapter

Done reading? Press Complete to save your progress. XP is Pro-only — on Pro this chapter is worth +10 XP.

Go Pro to earn XP →