Capstone: Messy App → Tested, Clean & Documented
Stage 3 · Testing, Quality & Craft · Capstone
You vibe-coded something real — it works, users love it, and you moved fast. Now comes the part that separates hobbyists from engineers: you go back in. You add the tests that make you fearless, clean the code that makes you cringe, write the docs that make the next person (future you, a collaborator, an AI pair) feel welcomed instead of lost. This capstone is your before-and-after. By the end, you'll open the repo and feel proud.
🎯 The mission
Take a real application you've already built — ideally something you vibe-coded in a hurry — and bring it up to a professional, shippable standard. You'll measure the gap between where it started and where it ends: lines of test coverage, number of code smells removed, whether a stranger could clone and run it from your README alone. The point isn't to rewrite. It's to lift what already works until it's something you'd show a senior engineer without apology.
🧱 What to do
Work through these layers in order. Each one builds on the last — don't skip ahead, because the tests you write first are what make every cleanup safe.
1. Pick your app and document the starting line
- Choose a real project: a vibe-coded front end from Stage 1, a backend from D1, a full-stack prototype — anything with logic worth protecting.
- Take a snapshot of the starting state: no tests, coverage at 0%, a list of 3–5 code smells you already know are there. Be honest; this is your baseline, not a judgment.
- Identify the critical path — the single sequence of steps a user takes to get the core value of the app (e.g., sign up → create item → see item listed). Everything else is secondary.
2. Set up the test runner and coverage
- Install and configure a test runner appropriate for your stack (Vitest, Jest, Pytest, Go's built-in
testing, etc.). - Add a
testandtest:coveragescript to yourpackage.json(or equivalent). - Verify you can run
npm testfrom a clean clone and see output. Zero passing tests is fine — the runner must work. - Set a coverage threshold in your config (start at 70% for the files you touch; raise it as you go).
3. Write the test suite — critical path first
- Unit tests for every pure function or isolated module on the critical path. Aim for fast, deterministic, no I/O.
- At least one integration test that wires two real layers together — e.g., a route handler calling a real database function (use a test database or an in-memory store).
- At least one end-to-end test that drives the critical path from the outside — a browser test with Playwright/Cypress, or a CLI test that calls your real server.
- Use test doubles (mocks, stubs, fakes) deliberately: isolate the unit under test; never mock what you own when an integration test would be more meaningful.
A minimal unit test to get the shape right:
// utils/formatPrice.test.js
import { formatPrice } from './formatPrice.js'
describe('formatPrice', () => {
it('formats whole dollars', () => {
expect(formatPrice(10)).toBe('$10.00')
})
it('rounds to two decimal places', () => {
expect(formatPrice(9.999)).toBe('$10.00')
})
it('returns $0.00 for zero', () => {
expect(formatPrice(0)).toBe('$0.00')
})
it('throws on negative input', () => {
expect(() => formatPrice(-1)).toThrow('Price cannot be negative')
})
})
Every assertion should be meaningful — it should fail for a real bug, not just to hit a number.
4. Refactor the worst module — under green tests
- Pick the single messiest module in the app (the one you dread opening). The tests you just wrote make this safe.
- Apply a clean code pass:
- Rename variables and functions to say what they are, not how they work.
- Extract magic numbers and strings into named constants.
- Replace nested
ifchains with guard clauses (return early). - Delete commented-out code and dead branches.
- Break functions longer than ~20 lines into smaller, named pieces.
- Run the tests after every meaningful change — if they go red, you introduced a bug; fix it before continuing.
- Write a short "before / after" comparison: paste the old function and the new one side by side. This becomes a deliverable.
5. Write / upgrade the README
Your README must let a complete stranger clone, configure, run, test, and deploy the app without asking you a single question. At minimum:
## Prerequisites
Node 20+, a Supabase project (or local Docker), an .env file.
Copy `.env.example` and fill in the values.
## Install
npm install
## Run locally
npm run dev # http://localhost:3000
## Test
npm test # unit + integration
npm run test:e2e # end-to-end (requires dev server running)
## Deploy
npm run build && npx vercel deploy --prod
That skeleton is the floor, not the ceiling. Add a description, a screenshot, environment variable docs, and a link to the live demo.
6. Write one Architecture Decision Record (ADR)
- Pick one non-obvious decision in the app — why you chose a particular library, how you structured authentication, why you split the data model the way you did.
- Write a one-page ADR: Context → Decision → Consequences. Store it at
docs/adr/001-<short-slug>.md. - ADRs are how future-you (and future collaborators) avoid relitigating decisions that were already made carefully.
7. Clean up git history and adopt a PR flow
- Ensure every commit from here forward follows Conventional Commits format:
feat:,fix:,test:,refactor:,docs:. - Create a branch for at least one of the changes you made above (
git checkout -b refactor/format-price-cleanup). - Open a pull request (even if it's just to your own
main). Write a PR description that explains why, not just what. - Review your own PR as if you were a stranger — would you merge this? Would the CI pass?
8. Self code-review against the Code Review Rubric
- Go through the Code Review Rubric from Lesson 9 line by line, treating your entire diff as the thing under review.
- Write a 1–2 page self-review: what passed, what you caught and fixed, what you deliberately left for a future ticket and why.
- This is not busywork — it's the habit that turns "it works" into "it's maintainable."
🗺️ Run it through B.U.I.L.D.
| Letter | Phase | What it means for this capstone |
|---|---|---|
| B | Blueprint | The critical path you mapped; the baseline you documented |
| U | Understand & Clean | The refactor pass — you can't clean safely without the tests |
| I | Implement & Integrate | The integration and e2e tests that prove the layers fit together |
| L | Launch | The README that makes it launchable by anyone, not just you |
| D | Document, Test, Deploy | Coverage report, ADR, self-review, PR — the whole D column |
The key insight: D and U are load-bearing here. The tests (D) are what make U (clean) safe. Every other step flows from that. Don't skip the order.
🧪 Deliverables
Submit all of the following — these are concrete artifacts, not vibes:
-
A passing test suite with a coverage report — screenshot or CI output showing the percentage. The critical path must be covered. Flakey tests don't count; fix them or delete them.
-
A before/after refactor document — the old function and the new function side by side, with a 2–3 sentence explanation of each change and why it matters.
-
A real README — passes the "stranger test": someone who has never seen your project clones it, follows your README, and has a running dev server within 10 minutes.
-
One ADR — a genuine Architecture Decision Record for a real decision in the app. Must include Context, Decision, and Consequences.
-
A self-review write-up — you ran the Code Review Rubric against your own diff and wrote up the results. At least one thing you caught and fixed; at least one thing you intentionally deferred with a reason.
-
A measurable improvement — compare your baseline to your final state on at least two of these axes: lines covered, number of named smells removed, README completeness, commit message quality. Numbers beat adjectives.
🏆 Stretch goals
These are not required, but they foreshadow D6 and separate good work from great work:
- Add CI — set up a GitHub Actions workflow that runs
npm testandnpm run test:coverageon every push and every pull request. A red test blocks the merge. This is what D6 (DevOps) is built on. - Get a real review — share your PR with a peer, a mentor, or an AI and ask for a formal code review. Respond to every comment, even if your response is "won't fix — here's why."
- Add a pre-commit hook — use Husky or a simple
.git/hooks/pre-commitscript that runs the linter and the unit tests before any commit is allowed. Fail fast, locally. - Achieve 90% coverage on the critical path module — not 90% overall (that's a trap), but 90% on the single most important file in the app.
- Write a second ADR — document a decision you made during this capstone (e.g., "why I chose Vitest over Jest").
✅ You're done when…
- The Code Review Rubric self-review is written and filed — you went line by line, you caught real things, and you documented what you fixed and what you deferred.
- The Pre-Ship Checklist is checked — tests passing, README accurate, no hardcoded secrets, coverage above threshold, conventional commits from here forward.
- The critical path is covered by meaningful tests — unit + integration + e2e all pass, assertions would catch real regressions, not just presence checks.
- The README lets a stranger run, test, and deploy the app — you tested this by following your own instructions from a fresh directory (or asked someone else to).
- The before/after refactor is documented — not just done, but explained: what changed, why, and what the tests proved stayed the same.
- The ADR exists and is honest — it captures a real decision, not an obvious one, and the Consequences section is candid about trade-offs.
A note on vibe coding: You got here because you could build fast. That skill is real and it matters — don't let anyone tell you otherwise. This capstone isn't about slowing down. It's about building a foundation that lets you go faster next time — because the tests catch the bugs before they catch you, the clean code is readable at 2 AM, and the README means your team (or your future self) never has to reverse-engineer what you built. The best engineers vibe-code the prototype and bring it to this standard. Now you do both.
➡️ Next: Stage 3 finishes with DevOps, Deployment & Operations — ship it and keep it alive. Build It Right, Or Don't Build It At All. 🏛️