Chapter 14 · Reps

The Front End That's Good Enough — Reps

← Back to Chapter 14

Chapter 14 — Reps

Conditioning, not grading. This week you put a real, clickable face on the API you have been building since Week 10. Browser HTML/CSS/JS reps.

Ground rules:

  1. Type every line yourself. Agents are ON this week (Phase 2), and you may let one scaffold a screenful of boilerplate — but you type the reps yourself and you must understand every line you keep. If you cannot explain a line, you did not earn it. Delete it and write it again.
  2. Run everything in a real browser. Open the page, open the developer console (the Network and Console tabs are your curl and your error log for the week). Watch requests fire. Read the errors.
  3. Work against your OWN API. Use the Week 10–13 backend you already built and persisted. These reps assume you have an endpoint that lists records (GET) and one that creates a record (POST). If yours uses different names than the examples, translate — that translation is part of the rep.
  4. Honesty floor is not optional. Every fetch you write handles its failure. A button that silently does nothing is a lie to the user.

You will need a way to serve the page over HTTP (not just open the file). The simplest, dependency-free option is in your terminal, from the folder holding index.html:

# Python (you already have it from Phase 1)
python3 -m http.server 5500
# then open http://localhost:5500 in your browser

Opening the file directly with file:// will appear to work but will make CORS and fetch behave strangely. Serve it. See the starter note in code/SERVER-NOTE.txt.


Reps 1–3: Just Enough HTML and CSS

Rep 1 — A page that exists

Create index.html with a valid <!DOCTYPE html>, a <head> with a <title>, and a <body> containing:

  • an <h1> naming the thing (use your own project’s domain),
  • a <p> of one sentence describing it,
  • a <form id="record-form"> with two labeled <input> fields and a submit <button>,
  • an empty <table id="records"> with a <thead> row of column headers and an empty <tbody>,
  • a <div id="error" class="error"></div> for messages.

Serve it with python3 -m http.server 5500 and open it. It will be ugly. That is correct. Confirm every element renders.


Rep 2 — Just enough CSS

Create styles.css and link it from your <head> with <link rel="stylesheet" href="styles.css" />.

Write no more than ~8 rules that make the page readable:

  • a body rule with max-width, margin: ... auto (to center the column), a system font, and line-height,
  • border-collapse and width: 100% on the table,
  • a border and padding on th, td,
  • an .error rule that makes error text red.

Then stop. Set a timer if you have to. Once the page is readable and usable, CSS is done. Do not fiddle. Write down one thing you were tempted to “improve” and chose not to — that note is the rep.


Rep 3 — Semantic vs. div soup

Rebuild the structure of your Rep 1 page using semantic elements where they fit: wrap the main content in <main>, the title area in <header>, use the heading/paragraph/list/table/form elements by meaning. Then, in a comment at the bottom of the file, answer in two sentences: what does a screen reader or an AI agent gain from <main> and <header> over <div>?


Reps 4–6: The DOM and Rendering Data

Rep 4 — GET and render

Create app.js, loaded with <script src="app.js" defer></script> at the end of <body>.

Write async function loadRecords() that:

  1. await fetch(...) your API’s list endpoint,
  2. checks res.ok and calls your error function if not,
  3. await res.json(),
  4. clears the <tbody> with replaceChildren(),
  5. loops the records, creating a <tr> with <td> cells via createElement and textContent, appending each to the <tbody>.

Call loadRecords() once at the bottom of the file. Reload the page. Your real database rows should appear in the table. Watch the request in the Network tab.


Rep 5 — textContent, not innerHTML

Add a record to your database (via curl or your existing tooling) whose text field contains literally: <b>Maya</b> <script>alert('x')</script>.

Reload your page. Because Rep 4 used textContent, the table must show that text verbatim as characters — not bold, not an alert. Confirm it.

Now, temporarily, change one cell to use innerHTML instead. Reload. Observe the difference (the <b> renders; do not be surprised if the script doesn’t run from innerHTML, but the markup injection is the point). Change it back to textContent. In a comment, name this vulnerability class and the Chapter 11 vulnerability it rhymes with.


Rep 6 — A refresh button and an event listener

Add a <button id="refresh">Refresh</button> to the page. In app.js, select it and addEventListener("click", loadRecords).

Add a record from another tool, then click Refresh in your page (without reloading). The new row should appear. You have just used the browser’s event loop — the same single-threaded event loop from Chapter 9. Write a one-sentence comment connecting the two.


Reps 7–8: Submitting Data with a Form

Rep 7 — Intercept and POST

Select your <form> and add a submit listener. As the first line, call event.preventDefault(). Then:

  1. build an object from form.fieldName.value for each input,
  2. await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(obj) }),
  3. check res.ok,
  4. on success: form.reset() then await loadRecords().

Type a record into the form, submit, and watch a brand-new row appear with no page reload — and confirm it actually persisted by reloading the page or checking your database. Browser → server → database → back, by your own hand.


Rep 8 — Break preventDefault on purpose

Comment out event.preventDefault(). Submit the form. Observe: the page reloads, the URL changes, your fetch never runs (or runs and is discarded by the navigation). This is the single most common front-end bug for beginners.

Restore event.preventDefault(). In a comment, explain in one sentence why the browser reloads without it.


Reps 9–10: CORS

Rep 9 — Cause a CORS error, then read it

If your front end (:5500) and your API (e.g. :3000/:8000) are on different ports — they are — you have probably already hit this. If your server currently allows CORS, remove the header/middleware so the error appears.

Reload and trigger a fetch. Open the Console tab and read the actual CORS error aloud. Copy the exact message into a comment. Confirm in the Network tab that the request may still appear but the response is blocked from your JS. Note: this is the browser, not your server, refusing.


Rep 10 — Fix CORS two ways

Way one (header): Add the appropriate CORS configuration on your server — Access-Control-Allow-Origin naming http://localhost:5500 (Node), or CORSMiddleware with that origin (FastAPI). Reload. The fetch now succeeds. If your POST triggers a preflight OPTIONS request, make sure your server allows the OPTIONS method and the Content-Type header.

Way two (same origin): As an alternative, make your server also serve index.html (and app.js, styles.css) so the page and API share one origin. Point your browser at the server’s own port. Observe that CORS disappears entirely because there is no longer a cross-origin request.

In a comment, state which of the two you would choose for your capstone, and why — in one sentence, in the right-tool language of this book.


Reps 11–12: The Honesty Floor and the Right-Tool Memo

Rep 11 — Loading, empty, and error states

Make your front end tell the truth in all three situations:

  1. Loading: before the GET resolves, show “Loading…” in the table area; clear it when data arrives.
  2. Empty: if the API returns zero records, show “No records yet.” in the table body — not a blank void that looks broken.
  3. Error: if res.ok is false or the network throws (wrap the fetch in try/catch), show a clear message in your #error box via textContent.

Test all three: load normally, point at an empty table, and stop your server and reload to force the error path. The honest MVP handles all three. This rep is the heart of the chapter.


Rep 12 — The “what I cut” memo (the architect’s rep)

Write a short frontend-decisions.docx (8–15 lines) for the front end you just built:

  • What does this front end do? (One sentence — the actual job.)
  • List three things you deliberately cut for the MVP, and one sentence each on why it was safe to cut.
  • State whether a framework (React/Vue/…) would be over-engineering here, and name the one condition under which it would become the right tool for this specific app.
  • Note one place an agent helped you and one decision you refused to delegate, and why.

This is the deliverable that proves you were the architect, not the assembler. It is the same muscle every Phase 2 project’s judgment call requires.


Done? One Last Thing.

Cold, no looking back at the chapter: open a blank index.html/app.js/styles.css and, against your own API, build the smallest possible honest front end — a table that lists records and a form that creates one — with res.ok checked, textContent used for data, event.preventDefault() on the form, and an error message on failure.

Time yourself. If you can build a usable, honest, full-stack front end on your own backend in under 30 minutes, from memory, with the error path included — you have the move. The face is true and the heart is sound. That is the whole week.


Up next: There is no project this week. Use the rest of it to write your one-page capstone scope (§14.11) — what it does, who it’s for, the right backend, the right database, the MVP front end and what you’ll cut, and where the agent helps versus where you hold the judgment. Then Chapter 15 — architecting the whole system.