Chapter 16 — Reps
Conditioning, not grading. This week the reps are a full-stack dress rehearsal — a timed run through the whole architect’s method on a throwaway scope, so that on the real capstone your fingers already know the path.
Ground rules:
- Type every line yourself unless a rep explicitly delegates it to the agent. Even in Phase 2, the muscle is hand-built first.
- Run everything. A server you didn’t start is a server you don’t understand.
curlevery endpoint. Click every button. - AI/agents are ON. This is Phase 2. But the architecture — the constraints, the stack choice, the data design — is yours, exactly as it will be in the capstone. Delegate the module bodies, not the decisions.
- Log as you go. Reps 9–11 build the agent-log habit you must already have by exam day. If logging in real time still feels like an interruption, you have practiced the wrong exam.
- Throwaway scope. Use a scope you are not submitting for the capstone, so the rehearsal is genuine practice and not a head start that tempts you to skip the architecture doc on the real thing.
Toolchain setup (Node, Python/FastAPI, the database CLIs, a static server for the front end) lives in Appendix A and Appendix B. The agentic-AI toolkit and the rules of delegation live in Appendix C. Everything below assumes you can start a server and open a browser without fighting your environment.
Reps 1–3: The Architecture, By Hand
These three reps are the part the capstone grades most. Do them on paper or in a markdown file, by hand, no agent. They take twenty minutes and they save the week.
Rep 1 — Name the constraints
Pick a throwaway scope (e.g., a tiny “small-group attendance” tracker). Write down, in five bullets:
- Who uses it and how many at once (one leader? a whole congregation?).
- What queries matter most (list by date? search by name? count attendance?).
- What consistency it needs (does a missed write matter? can two people write at once?).
- What scale it must reach (dozens of rows? millions?).
- What you / your team already know.
Five honest bullets. This is the input to every decision that follows. If you can’t answer them, you can’t choose a tool — you can only guess.
Rep 2 — Choose the stack, justified
From the five constraints, write three lines:
Backend: FastAPI — because validation matters and I want the auto-docs as the API spec.
Database: SQLite — because single-leader, dozens of rows, zero-config; Postgres would be over-engineering.
Front end: HTML/JS — because one leader on one screen; no framework earns its weight here.
Each line is a choice plus a constraint-based reason. “Because it’s popular” is not a reason. “Because the constraint from Rep 1 says X” is a reason. This is the thesis in three lines.
Rep 3 — Draw the system and design the data
Two parts, still by hand:
- The drawing. Boxes and arrows.
Browser (fetch) → API (GET/POST) → SQLite (one table). Label the seams. - The data. Write the schema (or document shape):
CREATE TABLE checkins (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
date TEXT NOT NULL, -- ISO 8601
present INTEGER NOT NULL DEFAULT 1
);
Name the columns, the types, the primary key, and any index you’d add and why. You now have a one-page architecture doc. That is the artifact the capstone weights most — and you wrote it in twenty minutes before any code existed. That is the lesson.
Reps 4–6: The Backend and the Database
Now build the layer that stores and serves. Data first — you can’t serve what you can’t store.
Rep 4 — Stand up the database layer
Create the table from Rep 3 and write the data-access functions by hand (no agent yet — you must know this layer cold). For SQLite in Python:
import sqlite3
from pathlib import Path
DB = Path("dress_rehearsal.db")
def init_db():
with sqlite3.connect(DB) as con:
con.execute("""
CREATE TABLE IF NOT EXISTS checkins (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
date TEXT NOT NULL,
present INTEGER NOT NULL DEFAULT 1
)""")
def add_checkin(name: str, date: str) -> int:
with sqlite3.connect(DB) as con:
cur = con.execute(
"INSERT INTO checkins (name, date) VALUES (?, ?)", # parameterized!
(name, date))
return cur.lastrowid
def list_checkins() -> list[dict]:
with sqlite3.connect(DB) as con:
con.row_factory = sqlite3.Row
return [dict(r) for r in con.execute("SELECT * FROM checkins ORDER BY date")]
Note the ? placeholders. Never concatenate values into SQL. Run init_db(), call add_checkin twice, call list_checkins, and print the result. Two rows.
Rep 5 — Wrap it in a JSON API
Now put an HTTP server in front of the store. FastAPI version:
from fastapi import FastAPI
from pydantic import BaseModel
import store # your Rep 4 module
app = FastAPI()
store.init_db()
class CheckIn(BaseModel):
name: str
date: str
@app.get("/litman-books/checkins")
def list_all():
return store.list_checkins()
@app.post("/litman-books/checkins", status_code=201)
def create(c: CheckIn):
new_id = store.add_checkin(c.name, c.date)
return {"id": new_id, **c.model_dump()}
Start it (uvicorn main:app --reload). Then curl both endpoints:
curl -X POST localhost:8000/checkins -H "Content-Type: application/json" \
-d '{"name":"Maya","date":"2026-05-30"}'
curl localhost:8000/checkins
Confirm the POST returns 201 and the GET returns your row. Open http://localhost:8000/docs and see the API documented for free.
Rep 6 — Prove persistence across a restart
This is the rep that catches the most common capstone bug. With a row in the database:
- Stop the server (Ctrl-C).
- Start it again.
curl localhost:8000/checkins.
The row must still be there. If it isn’t, your “database” is secretly an in-memory list — find it and delete it. Persistence means the data outlives the process. Prove it, every time, before you call a layer done.
Reps 7–8: The Front End
The face goes on last. A reasonable front end, not a beautiful one.
Rep 7 — One page that talks to your API
Write a single index.html that lists check-ins and adds one. By hand:
<!DOCTYPE html>
<html>
<body>
<h1>Check-ins</h1>
<form id="f">
<input id="name" placeholder="name" required>
<input id="date" type="date" required>
<button>Add</button>
</form>
<ul id="list"></ul>
<script>
const API = "http://localhost:8000";
async function load() {
const res = await fetch(`${API}/checkins`);
const rows = await res.json();
document.getElementById("list").innerHTML =
rows.map(r => `<li>${r.name} — ${r.date}</li>`).join("");
}
document.getElementById("f").onsubmit = async (e) => {
e.preventDefault();
await fetch(`${API}/checkins`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: document.getElementById("name").value,
date: document.getElementById("date").value
})
});
load();
};
load();
</script>
</body>
</html>
Serve it (a static server on a different port from the API). Open it. Add a check-in. See it appear.
Rep 8 — Find and fix the CORS error
If your front end is on localhost:5500 and your API is on localhost:8000, Rep 7’s fetch probably failed with a CORS error in the browser console. Good — find it. Then fix it on the backend:
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5500"], # your front-end origin
allow_methods=["*"], allow_headers=["*"],
)
Reload. The seam now works. The point of this rep: CORS is a seam bug, and seam bugs are where the capstone breaks. Decide your CORS story in the architecture doc, not live in front of a grader.
Reps 9–11: Directing the Agent (and Logging It)
Now bring the agent in — for module bodies, not decisions — and build the logging habit cold.
Rep 9 — Delegate one endpoint, scoped
Ask your agent to add a GET /checkins?date=YYYY-MM-DD filter to your existing API. Scope it tightly:
“Add a query parameter
dateto the existingGET /checkinsendpoint inmain.py. When present, return only check-ins on that date. Change only that endpoint and the correspondingstorefunction. Show me the diff before editing anything else.”
Run it. Then review every line for the Phase 2 failure modes: did it parameterize the SQL, or concatenate? Did it break the no-filter case? Did it touch anything you didn’t ask about? Diff before accepting.
Rep 10 — Log that delegation in real time
Immediately — not later — write the agent-log entry:
## Task 1 — Add date-filter to GET /checkins
- **Delegated:** add an optional `date` query param + filtered store query.
- **Agent did:** added the param and a `WHERE date = ?` query (parameterized, good).
- **Where it went wrong:** first attempt also changed the response shape to wrap in {"results": [...]},
breaking the front end's `rows.map`. I rejected that and re-prompted to keep the bare array.
- **I decided:** keep the flat array; the front end contract is fixed.
- **Verified:** curl with and without ?date= ; both correct.
The specific, auditable entry above is the standard. “Agent added a filter, worked great” is worth nothing. The capstone’s agent-log.txt is exactly this, repeated for every task.
Rep 11 — Catch the agent being wrong on purpose
Ask the agent for something it tends to get subtly wrong, then catch it:
“Add a UNIQUE constraint so the same name can’t check in twice on the same date, and handle the violation in the API with a 409.”
The agent will usually add the constraint. It often forgets to catch the resulting database error and lets the server 500 (or crash) on the duplicate. Test the duplicate path. If it crashes, you caught it — re-prompt to handle the IntegrityError at the boundary and return 409. Log the catch. This is the whole skill: the agent builds fast and is frequently subtly wrong, and you are the reviewer who ships only what survives review.
Done? One Last Thing.
From a cold start, with a clock running, do the whole architect’s method in 45 minutes on a second throwaway scope (e.g., a “memory-verse of the day” rotator):
- Five constraint bullets (Rep 1).
- Three-line justified stack (Rep 2).
- A drawing and a schema (Rep 3).
- One table, two data functions (Rep 4).
- A two-endpoint API (Rep 5),
curl-verified. - A one-page front end that lists and adds (Rep 7).
- A persistence-across-restart check (Rep 6).
- Two agent-log entries (Reps 9–11).
Forty-five minutes, end to end, browser to database. If you can do that cold — architecture first, then build, then log — the capstone is the same shape with a week instead of an hour and a defense at the end. The second run of this drill, on a third scope, is worth more than the first. Do it twice.
If you find the architecture-first order hard to hold under the clock: that’s the gap. It is the exact gap the capstone is built to expose. Close it now, on a throwaway, where it’s free.
Up next: Project 14 — Project 14: A Real Internet-Aware Application — the final project of the book. Read it now. Read it again before you start the build.