The Front End That's Good Enough
What face do we present to the world?
Chapter 14 — The Front End That’s Good Enough
“Simplicity is prerequisite for reliability.” — Edsger W. Dijkstra
“For the Lord sees not as man sees: man looks on the outward appearance, but the Lord looks on the heart.” — 1 Samuel 16:7
Why This Matters
You have a backend. You have a database. You have spent five weeks — Node, FastAPI, SQLite, Postgres, Mongo — building a server that takes a request and gives back JSON, and persists data that survives a restart. It is real. It works. You can prove it with curl.
And no human being on earth can use it.
That is the gap this week closes. Your mother cannot curl. Your pastor cannot read a JSON response. The volunteer who is going to run the church’s event sign-up does not have a terminal open, and if she did, she would not type curl -X POST -H "Content-Type: application/json" -d '{"name":"..."}' to sign up for the potluck. Between your beautiful API and an actual person there is a missing piece, and the missing piece is a thing you can click.
This chapter teaches you to build that thing. Not beautifully. Not with a framework. Not the way a front-end specialist with three years of React would build it. We are going to build a front end that is honest, usable, and shippable — and we are going to be unapologetic that “good enough” is a real engineering target, not a cop-out.
Here is the right-tool framing for the week, stated plainly. A front end is a cost, the same as any data structure or database engine. A beautiful front end that never ships costs you everything — the whole backend goes unused. A plain front end that ships this week costs you a weekend and delivers the entire value of the system to a real person. The architect’s job is to know which one the situation calls for. For an MVP — the thing you ship to find out whether the thing is worth building at all — the plain one wins almost every time. We will talk, honestly, about the cases where it doesn’t.
And there is a Christian question riding underneath, which is why the epigraph is what it is. Man looks on the outward appearance, but the Lord looks on the heart. The front end is the face your system shows the world. The temptation — the very old temptation — is to make the face beautiful and the heart hollow: a whitewashed tomb (Matthew 23:27), gorgeous on the outside, dead within. The opposite temptation, the one engineers fall into, is to be so proud of the heart that you present no face at all, and let the work die unseen and unused because you were too good for HTML. Neither is faithful. The faithful thing is a true face on a sound heart: an honest presentation of work that is actually good. That is what we are building.
This is also exactly where agentic AI earns its keep — and exactly where it cannot replace you. An agent will generate a hundred lines of correct, boring HTML/CSS/JS faster than you can type the opening <div>. Let it. That boilerplate is the masonry, not the architecture. But what the UI must do — which actions a human needs, what happens when the request fails, what the person sees when there is nothing yet to show — that is judgment, and that is yours. You decide the face. The agent paints it.
14.1 — The Browser Is Just Another Client
Stop thinking of “the website” as a separate thing from “the API.” It isn’t. The browser is a client of your API, in exactly the same way curl was a client of your API, in exactly the same way a mobile app or another server would be.
Here is the full picture you have been building, end to end:
[ Browser ] --fetch()--> [ Your Server ] --query--> [ Your Database ]
(HTML/CSS/JS) (Node / FastAPI) (SQLite/PG/Mongo)
^ |
|---------------------- JSON ------------------------------|
A person clicks a button in the browser. JavaScript running in that browser makes an HTTP request — the same GET and POST you have been sending with curl — to your server. Your server does what it always did: runs a query against your database, builds a response, sends back JSON. The JavaScript receives that JSON and turns it into something the person can see — a list, a table, a confirmation message.
That last step is the only genuinely new thing this week. Everything to the left of the browser, you already built. The browser is a new client. It speaks the same protocol. It hits the same endpoints.
Coach’s Note — If you internalize one sentence this week, make it this: the front end calls your API; it does not replace it. Students who skip this end up trying to put database logic in the browser, or duplicating validation, or sending SQL from JavaScript. No. The browser asks; the server decides; the database remembers. The seam between browser and server is the same HTTP seam you have been testing with
curlfor five weeks. You are adding a client, not rebuilding the system.
There are three languages in the browser, and they do three jobs. Keep them straight:
| Language | Job | Analogy |
|---|---|---|
| HTML | Structure and content — what is on the page | The skeleton |
| CSS | Presentation — what it looks like | The skin and clothes |
| JavaScript | Behavior — what it does when you interact | The muscles and nerves |
You will write a little of each. You will write enough of each. We are not turning you into a designer.
14.2 — Just Enough HTML
HTML is a tree of nested tags. That is the entire mental model. A tag opens with <name> and closes with </name>, and tags nest inside tags to form a tree — the same tree shape you built by hand in Chapter 6. The browser parses this tree (it calls it the DOM, the Document Object Model) and draws it.
Here is a complete, valid HTML page. Type it into a file called index.html and open it in your browser:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Event Sign-Up</title>
</head>
<body>
<h1>Potluck Sign-Up</h1>
<p>Add your name and what you're bringing.</p>
</body>
</html>
Four things to understand and then move on:
<!DOCTYPE html>tells the browser “this is modern HTML.” Always the first line. Never think about it again.<head>holds metadata — the page title, the character set, links to CSS. The user does not see the head.<body>holds everything the user does see.- Tags nest and must close.
<body>opened first, so it closes last. This is a tree, and like every tree, it has a strict parent/child shape.
Semantic elements: say what you mean
You could build an entire page out of <div> (a generic box) and <span> (a generic inline span). Don’t. HTML has elements whose names describe their meaning, and using them makes your page readable to you, to screen readers, and to the AI agent you will ask to modify it later.
| Element | Meaning |
|---|---|
<h1>–<h6> | Headings, <h1> most important |
<p> | A paragraph |
<ul> / <ol> / <li> | Unordered / ordered list, and list items |
<table> / <tr> / <th> / <td> | A table, a row, a header cell, a data cell |
<form> | A group of inputs the user submits |
<input> / <button> / <label> | A field, a clickable button, a label for a field |
<main> / <header> / <footer> / <section> | Document regions, by meaning |
A table is the workhorse of an MVP. When your API returns a list of records, a table renders it honestly with zero design effort:
<table>
<thead>
<tr><th>Name</th><th>Bringing</th></tr>
</thead>
<tbody>
<tr><td>Maya</td><td>Bread</td></tr>
<tr><td>Marcus</td><td>Salad</td></tr>
</tbody>
</table>
Forms: how a human sends you data
A form is how a person hands data to your application. Each <input> has a name (what the field is called) and the user supplies the value. A <label> tied to an input by for/id makes the form usable and accessible:
<form id="signup-form">
<label for="name">Name</label>
<input id="name" name="name" type="text" required />
<label for="dish">Bringing</label>
<input id="dish" name="dish" type="text" required />
<button type="submit">Sign Up</button>
</form>
Note required — that is the browser doing free, client-side validation for you. Note also: client-side validation is a convenience, not a defense. Your server still validates everything, because anyone can bypass the browser (you did, with curl). The form makes the honest user’s life easier; the server protects you from the dishonest one. You learned this in Chapter 10. It is still true.
Coach’s Note — We are going to intercept this form’s submission with JavaScript rather than letting the browser submit it the old-fashioned way (a full page reload to a server-rendered page). That older model is completely valid and, for many real apps, the simpler right tool. But because your backend speaks JSON and not HTML, intercepting with
fetch()is the natural fit here. Notice that this is a right-tool decision, made by the shape of the backend you already built.
14.3 — Just Enough CSS
CSS makes the page readable. That is the bar for this week: readable, not designed. You are a programmer building an MVP, not a visual designer building a brand. A page that is plain and legible beats a page that is half-styled and broken.
CSS is a list of rules. Each rule is a selector (what to style) and a block of properties (how to style it):
body {
font-family: system-ui, sans-serif;
max-width: 40rem;
margin: 2rem auto;
line-height: 1.5;
}
table {
border-collapse: collapse;
width: 100%;
}
th, td {
border: 1px solid #ccc;
padding: 0.5rem;
text-align: left;
}
.error {
color: #b00020;
}
Five selectors, and you have a page a human can read. Selectors come in a few kinds:
| Selector | Matches |
|---|---|
body | Every <body> element (a tag selector) |
.error | Every element with class="error" (a class selector) |
#signup-form | The element with id="signup-form" (an id selector) |
th, td | Both <th> and <td> (a comma-separated list) |
That single CSS trick — max-width on the body plus margin: auto — centers your content in a readable column and is, honestly, 80% of what “looks designed” means to a non-designer. Add legible spacing and a sensible font, and stop. You have hit “good enough.”
Coach’s Note — The single most common way programmers waste a week here is fiddling. You will be tempted to nudge a margin, change a color, then another color, then center one more thing. That is not engineering; it is anxiety with a stylesheet. Set a hard rule: the page must be readable and usable, and once it is, you are done with CSS. Ship it. The heart of the system is the API and the data, not the shade of the buttons.
14.4 — JavaScript in the Browser: The DOM
The DOM is the live, in-memory tree of your page. JavaScript can read it, change it, add to it, and listen for events on it. This is the same JavaScript you wrote for Node in Chapter 9 — same language, same const/let, same functions, same async/await. The difference is the environment: in Node you had http and the filesystem; in the browser you have the document and the window.
Selecting elements
You reach into the tree with document.querySelector (returns the first match) and document.querySelectorAll (returns all matches). The argument is a CSS selector — the same syntax you just learned:
const form = document.querySelector("#signup-form"); // by id
const rows = document.querySelectorAll("tbody tr"); // all rows in tbody
const tbody = document.querySelector("table tbody");
Creating and inserting elements
To render data, you create elements and attach them to the tree:
const tr = document.createElement("tr");
const nameCell = document.createElement("td");
nameCell.textContent = "Maya"; // textContent, NOT innerHTML — see below
tr.appendChild(nameCell);
tbody.appendChild(tr); // now it's on the page
Coach’s Note — Use
textContent, notinnerHTML, when you are inserting data that came from a user or your database.innerHTMLinterprets its string as HTML — so if a user signed up as<script>steal()</script>,innerHTMLwould run it. That is cross-site scripting (XSS), the browser cousin of the SQL injection you learned to defend against in Chapter 11. The defense is the same idea: treat data as data, never as code.textContentsets text and only text. Make it your default. Reach forinnerHTMLonly with content you fully control.
Listening for events
The page does nothing until the user does something. You attach an event listener — a function the browser calls when an event happens (click, submit, input, …):
const button = document.querySelector("#refresh");
button.addEventListener("click", () => {
console.log("button was clicked");
});
This is the event loop again — the central theme from Chapter 9. The browser, like Node, is a single-threaded event loop. Your JavaScript registers callbacks and then gets out of the way. When the user clicks, when a network response arrives, when a timer fires — the loop picks up the matching callback and runs it. You never block. You never poll. You register interest and let the loop call you back. Same model, different building.
14.5 — fetch(): Calling Your Own API
Here is the bridge. fetch() is the browser’s built-in way to make an HTTP request — the browser’s curl. It returns a Promise, so you use the async/await you learned in Chapter 9.
GET: list and render
To show the current sign-ups, GET them and render each into a table row:
async function loadSignups() {
const res = await fetch("http://localhost:3000/signups");
if (!res.ok) {
showError(`Server returned ${res.status}`);
return;
}
const signups = await res.json(); // parse the JSON body
const tbody = document.querySelector("#signups tbody");
tbody.replaceChildren(); // clear old rows before re-rendering
for (const s of signups) {
const tr = document.createElement("tr");
const name = document.createElement("td");
const dish = document.createElement("td");
name.textContent = s.name;
dish.textContent = s.dish;
tr.append(name, dish);
tbody.appendChild(tr);
}
}
Read that carefully, because three things in it are the whole pattern:
await fetch(...)— send the request, pause this function (not the browser) until the response headers arrive.res.ok/res.status— check before you trust. Afetchonly rejects on a network failure; a404or500is a perfectly “successful” fetch that returned a bad status. You must checkres.okyourself. This trips up everyone once.await res.json()— the body arrives separately and is also async; parse it into a real JavaScript object.
POST: submit a form to create
When the user submits the form, you intercept it, build a JSON body, and POST it to the same API you tested with curl:
const form = document.querySelector("#signup-form");
form.addEventListener("submit", async (event) => {
event.preventDefault(); // STOP the browser's default page reload
const body = {
name: form.name.value,
dish: form.dish.value,
};
const res = await fetch("http://localhost:3000/signups", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body), // object -> JSON string
});
if (!res.ok) {
showError(`Could not save: ${res.status}`);
return;
}
form.reset(); // clear the inputs
await loadSignups(); // re-render the list with the new row
});
That event.preventDefault() is load-bearing. Without it, the browser does what browsers have done since 1995 — submits the form by navigating the whole page — and your JavaScript never runs. Intercept first, then take over.
Showing errors honestly
Notice every fetch path above handles failure. This is not optional polish; it is honesty. A front end that silently does nothing when the server is down is lying to the user — it looks like the click worked when it didn’t. The honest MVP shows the failure:
function showError(message) {
const box = document.querySelector("#error");
box.textContent = message; // textContent, again
}
Coach’s Note — The error path is the part agents and rushing students skip, and it is the part that separates an honest front end from a deceptive one. A button that appears to work but silently fails is a whitewashed tomb — beautiful surface, dead inside. The user trusts the face you showed them. Make the face tell the truth: show the spinner, show the error, show the empty state. It is three lines of code and it is the difference between a tool people trust and a tool that quietly burns them.
14.6 — CORS: The Thing That Will Bite You
You will write all of the above, open your page, click the button — and see nothing, with a red error in the browser console that says something about CORS and “Access-Control-Allow-Origin.” Every full-stack beginner hits this. Here is what it is and how to fix it, once.
An origin is the triple (scheme, host, port) — e.g. http://localhost:5500. By default, JavaScript on a page from one origin is forbidden by the browser from reading responses from a different origin. Cross-Origin Resource Sharing (CORS) is the protocol by which a server says “it’s OK, this other origin may read my responses.”
The bite happens because in development your front end and your API are usually on different ports — say the page is served from http://localhost:5500 and the API listens on http://localhost:3000. Different port means different origin. The browser blocks it. This is a browser security rule (it protects users from a malicious page silently reading your bank’s API using your logged-in cookies); curl never enforced it, which is why you never saw it before.
The fix is on the server — it must send a header permitting your front end’s origin.
In Node (Chapter 9 style), add the header to your responses:
res.setHeader("Access-Control-Allow-Origin", "http://localhost:5500");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
In FastAPI (Chapter 10 style), add the middleware:
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5500"],
allow_methods=["GET", "POST"],
allow_headers=["Content-Type"],
)
| Symptom | Cause | Fix |
|---|---|---|
| ”blocked by CORS policy” in console | Front end and API on different origins | Add Access-Control-Allow-Origin on the server |
Works in curl, fails in browser | curl doesn’t enforce CORS; the browser does | The problem is real; fix the server header |
POST fails with a mysterious OPTIONS request first | Browser sent a preflight check | Allow the OPTIONS method and Content-Type header |
Coach’s Note — Two honest cautions. First:
Access-Control-Allow-Origin: *(allow everyone) works and you will be tempted to paste it. For a learning MVP behind localhost, that is acceptable. For anything real, name the exact origin you trust —*means any website can call your API from a victim’s browser. Second: the cleanest way to avoid CORS entirely is to serve your front end from the same origin as your API — have your server hand out theindex.htmltoo. Same origin, no CORS, no header. That is often the right tool for an MVP, and it is a one-line decision you should make on purpose.
14.7 — The MVP Front-End Philosophy: Good Enough on Purpose
Now the architecture lesson, which is the real point of the week.
“Good enough” is not a euphemism for “lazy.” It is a target, chosen deliberately, the same way you chose SQLite over Postgres in Chapter 11 when the problem didn’t need a server. The MVP — Minimum Viable Product — is the smallest thing that delivers the actual value, so you can find out, cheaply, whether the value is real before you spend more.
For a front end, “viable” means three things, in this order:
- Honest — it tells the truth. Successes look like successes, failures look like failures, “nothing here yet” looks like “nothing here yet.” No silent lies.
- Usable — a real person can accomplish the real task without instructions.
- Shippable — it exists, it is deployed, a human can reach it this week.
Everything beyond those three is a cost you are choosing to defer. And here is the architect’s discipline: decide what to cut, on purpose, and write it down.
What you cut from an MVP front end, almost always:
| Cut | Why it’s safe to cut for an MVP |
|---|---|
| Custom visual design / branding | Readable beats branded; brand can come after product-market fit |
| Animations and transitions | Pure polish; zero functional value |
| Client-side routing / SPA navigation | One page does the job; multi-page is fine |
| A component framework (React/Vue/…) | Big dependency, build step, learning cost — see below |
| Offline support, optimistic updates | Real features with real cost; rarely the MVP’s job |
| Perfect mobile responsiveness | ”Readable on a phone” is enough; pixel-perfect is later |
What you do not cut, ever:
- Error handling (the honesty floor).
- Server-side validation (the security floor — the browser is not a defense).
- An empty state (“No sign-ups yet” beats a blank void that looks broken).
Where a framework would be over-engineering — and where it would be the right tool
We are not teaching React in this book, and that is a deliberate right-tool call, not an omission. Let me be honest about both sides, because the architect’s skill is knowing where the line is.
A framework like React, Vue, or Svelte exists to manage complex, stateful UI — dozens of interacting components, state that changes in many places at once, large teams who need a shared structure. For that problem, a framework is genuinely the right tool: hand-written DOM manipulation does not scale to a thousand-component app, and pretending otherwise would be its own kind of malpractice.
But for an MVP that lists some rows and submits a form? A framework is a textbook case of over-engineering — solving a problem you do not have, at a cost you can feel:
| Cost of a framework for a small MVP | What it buys you on a small MVP |
|---|---|
A build step (bundler, transpiler, node_modules) | Nothing your 60 lines of JS needed |
| Hundreds of dependencies to audit and update | A supply-chain surface you didn’t need |
| A new mental model (JSX, hooks, reconciliation) | Power you won’t use at this size |
| Minutes-to-hours of setup before line one of your logic | Time you could have shipped in |
The plain fetch + DOM you learned this chapter is ~60 lines, zero dependencies, no build step, and ships today. That is the right tool for this size of problem. When the problem grows — real interactive state, a team, a product that has proven its worth — reach for the framework then, deliberately, having counted its cost. The framework is not wrong; using it for a to-do list is. That sentence is the whole book in miniature.
Coach’s Note — A junior reaches for React because it is what they learned and it feels “professional.” A senior reaches for plain HTML/JS for the MVP and says why, and reaches for React when the state complexity actually demands it and says why. The skill is not knowing React. The skill is knowing which problem you have. You learned to do this with databases; it is the identical move with front ends.
14.8 — Where the Agent Shines, and Where You Don’t Let It
This is the chapter where agentic AI is at its most useful and most dangerous, because UI boilerplate is exactly the kind of voluminous, patterned, correct-by-template code that agents produce beautifully — and what the UI must do is exactly the kind of judgment they cannot supply.
Let the agent build: the HTML scaffold, the CSS reset and table styling, the repetitive createElement row-rendering, the fetch plumbing, the CORS header lines. Ask for it by the screenful. It will be faster and cleaner than you typing it, and you can read every line because this chapter taught you the vocabulary. This is masonry. Delegate it.
You decide, always:
- What actions the UI exposes. Which buttons exist at all. An agent will happily build a “delete everything” button if you let it; whether that button should exist is a product decision, and it is yours.
- What the failure and empty states say. The honesty floor is a human judgment about what the user deserves to know.
- What to cut. The MVP scope — the entire §14.7 decision — is the architect’s call. An agent left unsupervised gilds; it adds the spinner animation and the dark mode toggle and the framework, because more looks like better. You cut on purpose.
- Whether the thing is honest. Only you can look at the finished face and ask: does this tell the truth about the heart? Man looks on the outward appearance. The agent only ever sees the outward appearance. You are responsible for the heart.
Coach’s Note — Your
agent-log.txtdiscipline from Phase 2 applies in full here, and this week it has a particular flavor: log not just what you delegated but what you refused to delegate and why. “I built the row-rendering with the agent; I decided the scope and the error-handling myself” is the sentence that proves you were the architect and not the assembler. That distinction is the whole of Chapter (Coding 2) 14 — the honesty question — made concrete.
14.9 — Common Bugs
Bug: Clicking submit reloads the whole page and your JavaScript seems to never run.
Example: A <form> with a submit listener that doesn’t call event.preventDefault().
Fix: Call event.preventDefault() as the first line of your submit handler. The browser’s default form submission is a full-page navigation; you must stop it before your fetch can take over.
Bug: fetch “succeeds” but you render nothing, or render undefined.
Example: Treating a 404/500 response as success because the fetch Promise resolved.
Fix: A fetch only rejects on a network error. Always check res.ok (or res.status) yourself before calling res.json(). A failed request is still a fulfilled Promise.
Bug: User-entered text breaks the page or runs unexpected behavior.
Example: cell.innerHTML = signup.name; where name contains < or > or a <script> tag.
Fix: Use element.textContent, not innerHTML, for any data from a user or the database. This is XSS defense — the browser sibling of SQL injection. Treat data as data, never as markup.
Bug: Everything works in curl but the browser console screams about CORS.
Example: Front end on :5500, API on :3000, no CORS header on the server.
Fix: Add Access-Control-Allow-Origin (naming your front end’s origin) on the server, or serve the front end from the same origin as the API. curl doesn’t enforce CORS; the browser does. The fix is always server-side.
Bug: Adding a row makes the table grow with duplicate old rows every refresh.
Example: Calling loadSignups() appends to <tbody> without clearing it first.
Fix: Clear the container before re-rendering — tbody.replaceChildren() (or set tbody.innerHTML = "", which is safe here because the string is empty and contains no user data).
Bug: The page is blank because the script ran before the HTML existed.
Example: A <script> in <head> that does document.querySelector("#form") before <body> is parsed, so it gets null.
Fix: Put your <script> tag at the end of <body>, or add the defer attribute (<script src="app.js" defer></script>). Either way the DOM exists before your code reaches for it.
14.10 — Reps
Open the exercises for the full set. AI/agents are ON this week (Phase 2), but the reps are hand-built — you type the HTML, CSS, and JS yourself, against the real API you built in Weeks 10–13, so the muscle is yours. The agent may scaffold; you must understand every line. There is no new project this week; the reps put a working front end on your existing backend.
A preview:
- Rep 1 — Build a page that exists: a static
index.htmlwith a heading, a form, and an empty table, served and opened locally. - Rep 4 —
GETyour API’s list endpoint and render the rows into the table. - Rep 7 — Intercept the form and
POSTa new record, then re-render. - Rep 9 — Hit and then fix a real CORS error against your own server.
- Rep 11 — Add the honesty floor: loading, empty, and error states.
Do every one against your own Week 10–13 API. By the end of the week the system you have been building for five weeks will, for the first time, be usable by a human being who is not you.
14.11 — This Week: Drills and Capstone Prep
There is no new project this week. The week has two jobs.
Job one: put a front end on the API you already have. Take the backend you built and persisted across Weeks 10–13 and give it a face — a single index.html + app.js + styles.css that lists your records in a table and creates new ones through a form, calling your own endpoints with fetch, handling errors honestly. The reps walk you through it. When you finish, your system is full-stack: browser to server to database and back. That is a milestone worth pausing on. Five weeks ago you could not write a line of server code; now you have shipped a thing a human can click.
Job two: scope your final. Project 14 — the capstone — is two weeks away and it is A Real Internet-Aware Application. Now is when you decide what it is. Use this week to write a one-page scope:
- What does it do, in one sentence? The actual job, the way you learned to state it in Chapter 1.
- Who is it for? A real person or ministry, concretely.
- What’s the right backend? Node or FastAPI — and why, in the right-tool language of this book.
- What’s the right database? SQLite, Postgres, or Mongo — and why. This is the judgment call no agent makes for you.
- What’s the MVP front end? What it must do, and — explicitly — what you will cut.
- Where will the agent help, and where will you keep the judgment? Draft the shape of your
agent-log.txtbefore you write a line.
Bring that one-pager to Chapter 15, where we architect the whole system end to end and harden the scope into a plan. The students who arrive at the capstone with a written, cut-down scope finish it. The students who arrive intending to “figure it out as I go” build a beautiful nothing that never ships.
14.12 — Coach’s Final Word for Week 14
You spent five weeks on the heart of the system — the server, the schema, the queries, the persistence that survives a restart. This week you gave it a face. And the lesson of the week is that the face must be true: honest about what the system does and does not do, usable by a real person, and shipped while it still matters.
Man looks on the outward appearance, but the Lord looks on the heart. The verse is a warning in both directions. Do not build a whitewashed tomb — a gorgeous front end over a hollow or dishonest backend. And do not despise the outward appearance so thoroughly that you present no face at all and let good work die unused. The faithful engineering is a true face on a sound heart: an honest, plain, usable presentation of work that is actually good. That is the front end that’s good enough. Good enough is not the floor you settled for. It is the target you aimed at on purpose, having counted the cost — which is the only kind of engineering this whole book has been teaching you to do.
Next week we put it all together — every layer, every seam — and we draw the system as a whole before we build the final.
See you on Monday.
Up next: Read the exercises and build a front end on your own API — every rep. There is no project this week; use the rest of it to write your capstone scope. Then Chapter 15 — architecting the whole system. (Previous chapter: Chapter 13.)