Web Foundations & Databases · beginner · ~11 min
- Trace a full page load from typing a URL to seeing rendered pixels, naming each actor in order. - Separate frontend responsibilities (structure, style, behavior) from backend responsibilities (logic, auth, data). - Read the four parts of an HTTP request and the three parts of an HTTP response. - Explain the client/server trust line and why every security decision must be re-checked on the server. - Predict what an attacker can and cannot control when they bypass the browser entirely. - Recognize the roles of DNS, TCP, and TLS in setting up the connection that carries a request.
Almost everything you do online is the same tiny conversation repeated millions of times: one side asks, the other side answers. The side that asks is the client — usually your web browser. The side that answers is the server — a program running on a machine somewhere else. The question is called a request, the answer is called a response, and the language they speak is HTTP.
This builds directly on HTTP, HTTPS, and TLS, your prerequisite lesson. There you learned the shape of an HTTP message and how TLS wraps it in encryption. Here we zoom out to the whole system: who sends messages, who runs which code, and — most importantly — who is allowed to trust what.
Think of ordering food through a delivery app. Your phone (the client) sends an order (the request). The restaurant's kitchen (the server) reads it, cooks, and sends food back (the response). You can scribble anything you like on the order slip, but the kitchen decides what it will actually make. The web works the same way: the browser can ask for anything, but the server decides what actually happens.
In plain language first: the frontend is the part you see and that runs on your own device, so you fully control it. The backend is the part that runs on the server, holds the real rules, and talks to the database. The single most important idea in this lesson is the trust line between them — the boundary that separates "code the user can change" from "code the user cannot touch."
The boundary between frontend and backend trust is the root of most web vulnerabilities. If you understand nothing else about web security, understand this line.
Hidden buttons, greyed-out fields, dropdowns that only list "allowed" choices, and JavaScript that checks a form before submitting — these all live in the browser, which the user completely controls. Any of them can be bypassed in seconds. An attacker does not have to use your carefully built form; they can craft the raw request by hand with curl, a proxy like Burp Suite, or the browser's own developer tools, and send whatever bytes they want straight to your server.
That is why the professional instinct is always: "What is enforced, and where?" A price shown in the page is a suggestion; the price the server charges is the fact. A role hidden in the UI is decoration; the permission the server checks is the control. Knowing what is enforced where is the core skill of both building secure web apps and testing them. Once you see the web as two zones separated by a trust line, an enormous number of bug classes — broken access control, price tampering, hidden-field abuse — suddenly look like the same bug: trusting the client.
Definition: A client is the program that initiates a request. A server is the program that waits for requests and returns responses. On the web the client is almost always a browser, and the server is a program (often called a web server or application server) listening on a network port.
Plain-language explanation: The client is the one who asks first. The server never speaks unless spoken to — it sits and listens, and when a request arrives it does some work and replies. One server answers many clients at once.
How it works internally: The client opens a network connection to the server's address, writes the bytes of a request, and waits. The server reads those bytes, runs code, and writes back the bytes of a response. Then, depending on the protocol version, the connection is reused or closed.
When this model applies / when it does not: This request/response model fits web pages, APIs, and most day-to-day traffic. It does not fit cases where the server needs to push data at any moment (live chat, stock tickers) — those use extra techniques like WebSockets or server-sent events layered on top.
Common pitfall: Believing the client and server are one program. They are two separate programs, often written in different languages, on different machines. Anything "the browser knows" is not automatically something the server knows, and vice versa.
CLIENT (browser) SERVER (backend)
+------------------+ +--------------------+
| HTML CSS JS | 1. request ---> | app logic |
| runs on YOUR | | auth checks |
| machine | <--- 2. response | database access |
+------------------+ +--------------------+
user can read & user NEVER sees this
change everything code, only its output
Knowledge check: In the diagram above, which side decides whether a "Delete account" action actually happens — and why?
Definition: An HTTP request is a message from client to server made of four parts: a method, a target (URL path), headers, and an optional body.
Plain-language explanation: The method is the verb — what you want to do. The path is the noun — what you want to do it to. Headers are metadata — extra facts like what format you accept or who you claim to be. The body is the payload — the actual data you are sending, used mainly with POST and PUT.
How it works internally: These four parts are just text (and optionally binary body bytes) laid out in a fixed order. The very first line names the method, path, and HTTP version. Then come header lines, then a blank line, then the body.
When to use which method: Use GET to read (it should never change data). Use POST to create or submit. Use PUT/PATCH to update, and DELETE to remove. Do not use GET for actions that change state — search engines and browsers pre-fetch GET links and could trigger the change by accident.
Common pitfall: Assuming the method or headers are trustworthy. The client writes all of them, so a request can claim any method, any header value, any body. The server must validate them.
GET /account/settings HTTP/1.1 <- method path version
Host: shop.example.com <-+
User-Agent: Mozilla/5.0 | headers
Accept: text/html |
Cookie: session=abc123 <-+
<- blank line ends headers
(body goes here, empty for GET)
Knowledge check: A form on a page only offers three shipping options in a dropdown. Can the server receive a fourth, unlisted option? Explain in your own words.
Definition: An HTTP response is the server's reply, made of three parts: a status line (with a status code), headers, and a body.
Plain-language explanation: The status code is a three-digit summary of what happened. The headers describe the reply (its type, length, caching rules). The body is the actual content — HTML, JSON, an image, whatever was requested.
How it works internally: The status line comes first (e.g. HTTP/1.1 200 OK), then headers, then a blank line, then the body bytes. The browser reads the status and headers to decide how to handle the body, then renders or processes it.
Status code families:
| Range | Meaning | Example |
|---|---|---|
| 1xx | Informational | 100 Continue |
| 2xx | Success | 200 OK, 201 Created |
| 3xx | Redirect | 301 Moved, 302 Found |
| 4xx | Client error (your fault) | 404 Not Found, 403 Forbidden |
| 5xx | Server error (their fault) | 500 Internal Server Error, 503 Unavailable |
When to trust a status code: The status reflects what the server decided. Unlike request fields, the client cannot fake a response status to itself in any meaningful way — the server authored it. This is why a 403 Forbidden from the backend is a real control, while a hidden button in the frontend is not.
Common pitfall: Treating any 2xx as "everything is fine." A 200 OK can still carry an error inside the body ({"error": "..."}). Read both the status and the body.
Knowledge check: You send a request and get
404 Not Found. Is the problem more likely in your request or in the server's code? Which status range would point the other way?
Definition: The frontend is the code that runs in the browser (HTML for structure, CSS for style, JavaScript for behavior). The backend is the code that runs on the server (application logic, authentication, database access).
Plain-language explanation: The frontend is the storefront window and the order slip — visible, touchable, user-facing. The backend is the kitchen and the safe in the back office — where the real decisions and valuables live.
How it works internally: The server sends frontend files (HTML/CSS/JS) to the browser, which downloads and runs them locally. From that moment those files are on the user's machine. The backend code never leaves the server; the user only ever sees the responses it produces.
| Frontend | Backend | |
|---|---|---|
| Runs on | user's device | the server |
| Languages | HTML, CSS, JavaScript | any (C, Python, Go, Java, ...) |
| User can read it? | yes (View Source, devtools) | no |
| User can change it? | yes | no |
| Good for | layout, interactivity, quick feedback | rules, auth, data, security |
When to put logic where: Put in the frontend anything that improves experience and can be safely wrong (instant "password too short" hints, showing/hiding panels). Put in the backend anything that must be correct or safe (who may do what, what the real price is, whether the input is valid). Backend checks are mandatory; frontend checks are a bonus.
Common pitfall: Doing a check only in JavaScript and assuming it protects the server. It does not — it protects nothing, because the user can skip the JavaScript entirely.
Definition: The trust line is the boundary between attacker-controlled code (the frontend) and trusted code (the backend). Everything crossing from client to server must be treated as untrusted input.
Plain-language explanation: Imagine a border between two countries. On the user's side, anything can be written on a passport. At the border booth (the server), a guard must actually verify it. The frontend hands over papers; the backend is the guard who checks them.
How it works internally: Every request arriving at the server was authored somewhere the server cannot see. Maybe by your real page, maybe by a script, maybe by an attacker with a proxy. The bytes look identical. So the server has no way to know the request "came from the real form" and must validate every field as if it were hostile.
When client-side checks are okay: As a convenience layer only — faster feedback, fewer accidental bad requests. Never as the only check.
Common pitfall: "Security by obscurity" — hiding a button, disabling a field, or removing a menu item and thinking the action is now protected. Hiding is not enforcing.
USER-CONTROLLED | TRUSTED
(do not trust) | (enforces rules)
|
browser, HTML, CSS, JS, | server code, auth,
devtools, curl, proxy ---> | validation, database
request | the ONLY place a
attacker can send ANY bytes | security decision counts
TRUST LINE
Knowledge check: Explain in your own words why removing an admin link from the navigation bar does not stop a normal user from reaching the admin action.
There is no C syntax here — the "syntax" of the web is the layout of HTTP messages. Both messages are plain text with the same shape: a first line, header lines, a blank line, then an optional body.
# REQUEST
METHOD /path HTTP/1.1 <- request line (verb, target, version)
Header-Name: value <- one header per line
Host: example.com <- Host is required in HTTP/1.1
<- blank line marks end of headers
body bytes (optional) <- present for POST/PUT, empty for GET
# RESPONSE
HTTP/1.1 200 OK <- status line (version, code, reason)
Content-Type: text/html <- describes the body
Content-Length: 42 <- byte count of the body
<- blank line marks end of headers
<html>...</html> <- body: the actual content
Key rules to remember: the first line differs between request and response, headers are Name: value pairs, and a single blank line always separates headers from the body.
A web app is a conversation between a client (the browser) and a server, carried over HTTP.
Frontend runs in the browser:
It is fully under the user's control. Anyone can read and modify it with browser devtools (the developer tools built into every browser).
Backend runs on the server:
The user never sees this code — only the responses it sends back.
Because the frontend is attacker-controlled, no security decision can live there.
JavaScript that "disables" a button, hides an admin link, or validates a form is a convenience, not a control. An attacker simply crafts the request directly — with curl, Burp, or devtools.
For example, hiding a "Delete user" button in the page does not stop anyone from sending the same delete request by hand.
Every check that matters must be re-done on the backend. This single idea underlies a huge fraction of web vulnerabilities.
The clearest way to see the client/server split is to be the client yourself. curl is a command-line HTTP client: it sends a raw request and prints the raw response, with no browser and no JavaScript in the way. That is exactly the attacker's-eye view — proof that the server, not the page, is what answers.
# 1. A plain GET request. -i tells curl to also print response headers.
curl -i https://httpbin.org/get
# 2. Watch the FULL conversation: -v shows the request we send (>)
# and the response we receive (<), including the status line.
curl -v https://httpbin.org/status/200
# 3. Send a POST with a body, exactly like a form submit -- but WITHOUT
# a browser, form, or any client-side validation. The server sees only bytes.
curl -i -X POST https://httpbin.org/post \
-H "Content-Type: application/json" \
-d '{"role": "admin", "price": 0}'
# 4. Ask for a path the server does not have, and observe the status family.
curl -i https://httpbin.org/status/404
What it does: httpbin.org is a free public testing service that echoes back whatever you send — perfect for a lab. Command 1 fetches a resource and shows both the response headers and body. Command 2's -v (verbose) prints every line of the exchange so you can read the request line, the status line, and each header. Command 3 sends a JSON body claiming role: admin and price: 0 — the point is that nothing stops us from sending this; only a real backend's validation would reject it. Command 4 requests a deliberately-missing status and returns 404.
Expected output (shape, from command 1):
HTTP/2 200
content-type: application/json
content-length: 257
...
{
"headers": { "Host": "httpbin.org", ... },
"url": "https://httpbin.org/get"
}
You will see a 200 status line, several headers, a blank line, then a JSON body. Exact header values and byte counts vary by request, so do not memorize them — recognize the structure.
Key edge cases: If you are offline you get a connection error, not an HTTP status (no server was reached). A redirect (3xx) will not be followed unless you add -L. A 200 status can still wrap an error message inside the body, so always read both.
Let's trace what happens when you run curl -v https://httpbin.org/status/200 — the moment a request becomes a response.
curl asks DNS to translate httpbin.org into an IP address (e.g. 54.x.x.x). Names are for humans; the network routes by number.curl opens a TCP connection to that IP on port 443 (the HTTPS port). TCP guarantees the bytes arrive in order.https, curl and the server negotiate encryption (the TLS you met in HTTP, HTTPS, and TLS). After this, everything is encrypted on the wire.curl writes the request line GET /status/200 HTTP/2, a Host: httpbin.org header, and a few defaults, then a blank line. -v prints these with a > prefix./status/200, decides its response, and builds a reply. This is the trusted zone — code you never see.HTTP/2 200 OK, headers, a blank line, and a (here empty) body. -v prints these with a < prefix.curl prints and exits. No rendering, no JavaScript — just the raw bytes.Here is how the important values change as the request travels:
| Step | Where we are | Key value at this moment |
|---|---|---|
| 1 | DNS | httpbin.org -> IP address |
| 2 | TCP | connection open to IP:443 |
| 3 | TLS | channel now encrypted |
| 4 | client writes | GET /status/200 HTTP/2 |
| 5 | server thinks | decides status = 200 |
| 6 | client reads | HTTP/2 200 + headers |
| 7 | done | exit code 0 |
The crucial takeaway from the trace: steps 4 and 6 are the only bytes that cross the network. Step 5 — the actual decision — happens entirely on the server, which is precisely why that is the only place a security check can be trusted.
Mistake 1: Relying on client-side validation for security.
// WRONG: the only check is in the browser
function submitOrder(price) {
if (price < 0) { alert("Invalid price"); return; } // user can delete this line
sendToServer({ price });
}
Why it's wrong: the attacker never runs your JavaScript. They send the request directly with a negative or zero price. The if protects nobody.
# CORRECTED (conceptual backend check, any language):
on receive order:
if price < 0 or price != catalog_price(item):
reject with 400 Bad Request
# only now proceed
Recognize/prevent it: for every client-side check, ask "is there a matching check on the server?" If not, it is not a security control.
Mistake 2: Hiding instead of enforcing. Removing a "Delete user" button or an admin menu item and assuming the action is now safe. The endpoint still exists; anyone who knows its URL can call it. Fix: enforce the permission on the server for that endpoint, regardless of what the UI shows.
Mistake 3: Using GET to change data. A link like GET /account/delete looks convenient, but browsers, crawlers, and link-preview bots pre-fetch GET URLs and can trigger the action unintentionally. Fix: use POST/DELETE for state changes, and require the user to have actually intended it.
Mistake 4: Trusting request headers. Assuming a User-Agent, Referer, or even a client-set X-Is-Admin header is genuine. All of these are written by the client and can say anything. Fix: derive identity and permissions from server-verified sources (like a validated session), never from arbitrary headers.
Mistake 5: Confusing 'the page shows X' with 'the server did X'. Seeing a success message in the UI does not prove the backend accepted it. Fix: confirm outcomes against the response status and, when it matters, the persisted data.
Because there is no compiler here, the errors you meet are network and logic errors. Work outward from the connection.
Connection errors (no HTTP status at all):
Could not resolve host — DNS failed; check the spelling of the domain and your internet connection.Connection refused / timeout — nothing is listening on that address/port, or a firewall blocked you. Verify the host and port (443 for HTTPS, 80 for HTTP).HTTP-level errors (you got a status, so the server answered):
4xx means your request was the problem. 400 = malformed, 401 = not authenticated, 403 = authenticated but not allowed, 404 = path doesn't exist. Re-read the request line and headers.5xx means the server's code failed. 500 = an unhandled error on their side, 503 = temporarily overloaded. You usually can't fix this from the client.Concrete debugging steps:
-v to curl (or open the browser's Network tab) and read the actual request bytes sent — often different from what you assumed.curl. If it works there but not in the page, the bug is in your frontend JavaScript; if it fails both ways, it's the request or the server.Questions to ask when it doesn't work: Did I even reach the server (status vs. connection error)? Is this a 4xx (my request) or 5xx (their code)? What exact bytes did I send? Does the same request succeed from curl?
This is a concept lesson, so there is no C memory to manage — but there is a direct analogy that will serve you when you do write C servers, and a security mindset worth stating plainly.
Treat every incoming byte as untrusted input. In C, a network request is just a buffer of bytes the remote side chose. The classic failures — reading past a buffer's end, trusting a length field the client sent, assuming a string is NUL-terminated, integer overflow in a size calculation — all begin with trusting attacker-controlled data. The web trust line and C memory safety are the same discipline: never assume the shape or size of input you did not create. Your related exercises (parsing an HTTP status line, building a GET request) are where you'll practice bounding those reads and writes with an explicit capacity (cap).
Defensive practices for this topic:
Vulnerability shown vs. fix (clearly labeled): In the Mistakes section, the JavaScript-only price check is a vulnerability — client-side-only validation. The fix is the matching server-side check that rejects any price not equal to the real catalog price. Client validation may stay for UX, but it is decoration, never a control. All of this stays lab-only: use test services like httpbin.org or your own local server, never someone else's system.
Concrete real-world use: Every website you use is this loop. Loading your bank's dashboard is a GET whose response the browser renders; transferring money is a POST the backend authorizes, validates, and records in a database — and the bank never trusts the amount just because the page displayed it. Mobile apps, smart TVs, and IoT devices are all HTTP clients too; the phone app and the website often talk to the same backend API, which is exactly why the backend, not any one client, must hold the rules.
Health checks and load balancers read only the status line of a response to decide if a server is alive — the motivation behind your HTTP status-line parser exercise. API integrations (a shop talking to a payment provider) are pure client/server request/response with no browser at all.
Professional best-practice habits:
| Habit | Beginner | Advanced |
|---|---|---|
| Validation | check required fields exist on the server | full schema validation, length/range limits, allow-lists |
| Correct methods | use GET to read, POST to change |
idempotency, proper PUT/PATCH/DELETE, safe retries |
| Error handling | return the right status code | consistent error bodies, no leaking internal details |
| Auth | check the user is logged in | per-request least-privilege authorization, session hardening |
| Readability | clear endpoint names (/orders/42) |
RESTful, versioned, documented APIs |
| Observability | read status codes when debugging | structured logging, tracing, metrics on every request |
The throughline: beginners make it work; professionals make it safe, clear, and correct under load — and both start from knowing exactly what is enforced where.
1. (Beginner) Label the message parts. Objective: cement the anatomy of HTTP. Take this request and response and label every part.
POST /login HTTP/1.1
Host: example.com
Content-Type: application/json
{"user":"amy"}
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{"error":"bad credentials"}
Requirements: identify the method, path, each header, the blank line, and the body in the request; and the status code, reason phrase, and body in the response. Hint: the first line is special in both. Concepts: request/response anatomy, status families.
2. (Beginner) Sort the responsibilities. Objective: internalize the trust line. Given this list, mark each as FRONTEND-ok, BACKEND-required, or BOTH: (a) show a red border on an empty field, (b) verify the user owns the order they're editing, (c) format a date nicely, (d) check the payment amount is positive, (e) hide an admin menu item. Hint: ask "what breaks if the user skips this?" Concepts: frontend vs backend, trust line.
3. (Intermediate) Be the client with curl.
Objective: observe a real exchange. Using curl -v against https://httpbin.org/get, capture the output and answer: what method and path were sent? What status came back? Name three response headers. Then repeat against https://httpbin.org/status/404 and note how the status line changes. Requirements: paste the request line and status line you saw. Constraint: lab service only. Hint: > lines are what you sent, < lines are what you received. Concepts: request/response, status codes, client role.
4. (Intermediate) Break your own validation.
Objective: prove client checks aren't security. Write a tiny HTML page with a JavaScript check that blocks submitting a quantity greater than 10, then send a request with quantity 999 without using the form (use curl or devtools). Describe what reached the "server" (use httpbin.org/post as a stand-in echo). Requirements: show that 999 got through. Hint: the form's JavaScript never runs when you skip the form. Concepts: trust line, client-side validation weakness, POST body.
5. (Challenge) Design the enforcement, not the UI. Objective: think like a backend engineer. For a simple "apply discount code" feature, write (in plain English or pseudocode) the server-side checks needed so the discount cannot be abused, assuming the attacker can send any request they want. Requirements: cover at least — code exists and is active, not already used by this user, applies to the items in the cart, and the final price is recomputed on the server (never trusted from the client). Constraint: no reliance on anything the frontend shows or hides. Hint: for each thing the UI 'guarantees,' add the matching server check. Concepts: trust line, server-side validation, least privilege, fail-closed.
200 can still wrap an error, and the only place a security decision counts is the backend. Prove it to yourself with curl — be the client and watch the raw bytes.