Ship Data Apps with AI

๐ŸŒฑ Start from zeroยทLesson 3 of 26ยท~25 minยทBeginner

๐Ÿค– Work with Claude Code

Prompts that get small, working results, a notes file the AI reads every session, and the habit of checking its work instead of trusting it.

You'll leave with

  • A prompt pattern: options first, the smallest version, then a way to check it
  • A NOTES.md per app that stops Claude repeating old mistakes
  • A rule for when the AI may touch production, and when it must stop and measure

In the last lesson, Claude Code, the AI assistant that works inside your project folder, built you a page. Now come the daily habits: how to ask so you get something small that works, how to make it remember what you decided, and when to stop trusting it and measure. It's how I build every app in my fleet, mistakes included.

A fast, confident intern with no memory

Claude Code runs in the terminal of VS Code, the free code editor from last lesson (the terminal is the panel at the bottom where you type commands). It's an agent, working in a loop:

What's an agent?

An AI that acts instead of only answering: it reads your files, edits them and runs commands. In manual mode (last lesson), Claude Code asks your permission before each edit or command.

you      say what you want, and how you'll check it
  โ†“
Claude   reads the files in your folder
  โ†“
Claude   proposes an edit or a command  โ†’  you approve, or say no and why
  โ†“
Claude   runs it, reads what happened, and goes round again until it thinks it's done
  โ†“
you      check the result yourself: the change, the page, the numbers

Three things about this intern matter most:

  • It's fast. It writes a page faster than you can read one.
  • It sounds sure, even when it's wrong. "Fixed it" reads the same either way.
  • It has no memory. Every session starts knowing nothing about yesterday.
What's a session?

One conversation with Claude Code, from typing claude until you close it. Each new one starts blank.

Your job is scope, decisions and verification. Its job is the typing. The rest of this lesson is how to do your three jobs with the least effort.

The prompt pattern that works

A prompt is the message you type to Claude. The ones that get me small, working results have four parts:

  1. Do and see: what a person can do, and what they see when it works.
  2. Out of scope: what not to build or touch.
  3. Smallest version: the least that counts as done.
  4. The check: how you'll know it worked.

Here it is with dark mode (CSS is a page's styling; JavaScript is the code that makes it do things):

โŒ The usual way

  • "Add dark mode."
  • Claude picks the scope, often the biggest one, and you can't tell whether it works, because you never said what working looks like.

โœ… The lazy way

  • "Add dark mode that follows the computer's setting: dark background, light text. No toggle, no JavaScript. I'll check by switching my computer to dark mode."
  • One small change, checked in ten seconds.

Two habits go with it. For anything touching more than one file, start in plan mode: press Shift+Tab (some Windows terminals: Alt+M) until the footer under Claude's input box says plan mode on. And read the diff after every change: VS Code's Source Control panel shows every changed line in red and green.

What are plan mode, a diff and a commit?

In plan mode, Claude can read files and write a plan but can't change anything until you approve it; Shift+Tab again cycles out. A diff is a file's before-and-after: removed lines in red, added in green. A commit is a saved snapshot in git, the history-and-undo tool from last lesson.

At a fork, ask for options

At a real choice, ask for two or three concrete options with trade-offs and a recommendation. Then you pick, and write down why. An open-ended question gets you the most elaborate build; options plus a recommendation get you the smallest one that works.

๐Ÿ“ Acento, my Argentine Spanish trainer, made its big calls this way: accounts now, with 10 free reviews a day, and payments later; accent scoring as a mode inside Acento, not a separate app. Each pick went into its notes with the reason, and I reopen one only when the reason changes.

1

Ask for options before any code

Practice on your hello-world page from Set up your workshop. Dark mode has a real fork, and you can check the result with your own eyes.

  1. In VS Code, open your myapp folder (File โ†’ Open Recent) and the terminal (View โ†’ Terminal). Type claude and press Enter.
  2. Press Shift+Tab until the footer says plan mode on, then send:

EXAMPLE โ€” YOU ASK CLAUDE CODE

Claude Code
YouBefore writing code, give me 2 or 3 options for dark mode on my page, with effort and trade-offs, smallest first, and your recommendation.
ClaudeI read index.html. 1. Follow the computer's setting: a few lines of CSS, no JavaScript; visitors can't override it. 2. A toggle that remembers each visitor's choice: some JavaScript, more to test. 3. Both. I recommend 1 now, and 2 only if a visitor asks. Plan mode is on, so nothing has changed.โ†ณ Read ยท index.html

What's happening underneath: in plan mode Claude can't change anything, so asking costs nothing. The recommendation is a suggestion; the pick is yours. Take option 1 and remember why (it needs no JavaScript): you'll write it down in step 3.

2

Build the smallest version, then check it

Answer in the same session, with all four parts:

EXAMPLE โ€” YOU ASK CLAUDE CODE

Claude Code
YouGo with option 1. Someone whose computer is in dark mode sees a dark background and light text. Out of scope: a toggle, any JavaScript, any new file. Smallest version: background and text colors. I'll check by switching my computer to dark mode and reloading.
ClaudePlan: add one CSS rule to index.html that applies only in dark mode. One file, no JavaScript. Go ahead?

Choose the option that has you approve each edit yourself (the wording varies by version). Before writing, Claude shows the edit as a diff, in the terminal or a VS Code tab. Look for added lines like these (don't paste them):

@media (prefers-color-scheme: dark) {
  body { background: #111; color: #eee; }
}

What's happening underneath: that rule asks the browser "is this computer in dark mode?" and applies its colors only if so. If the diff adds a <script>, a button or a new file, Claude went past the scope: choose No and say "CSS only". Approved an edit by mistake? /rewind can undo Claude's file edits, though not what a command did.

Check it yourself, on your computer (a phone can't open your laptop's localhost):

  1. If the page isn't open, click the + at the top right of the terminal panel for a second terminal, run npx serve, and open the http://localhost:3000 address it prints.
  2. Switch your computer to dark mode (Windows: Settings โ†’ Personalization โ†’ Colors โ†’ Choose your mode; Mac: System Settings โ†’ Appearance) and hard-reload: Ctrl+Shift+R (Mac: Cmd+Shift+R; Safari: Cmd+Option+R). Dark background, light text; switch back and the page follows. (Still light? Your browser's own Appearance settings may force a light theme.)
  3. Open Source Control, the icon on the far left with three small circles joined by lines (Ctrl+Shift+G; Mac: Control+Shift+G), and click index.html under Changes. Only it should be marked M (modified); U means a new file, like last lesson's CLAUDE.md on Windows.
  4. Ask Claude: Commit this as "Dark mode follows the system setting" and show me git log --oneline -3. The newest line should be your message.

A notes file per app

Claude forgets; a file doesn't. Of the files in your project, Claude Code reads one by itself at the start of every session: CLAUDE.md. Type /init once to create it, then add a line containing only @NOTES.md. That import makes Claude load your notes every session, so your decisions, lessons and skipped list actually reach it. Step 3 does both.

What are CLAUDE.md and NOTES.md?

Plain text files in Markdown (.md), where # starts a heading and - a list item. Claude Code loads CLAUDE.md every session by itself; the @NOTES.md line pulls in NOTES.md, which is yours.

Every app in my fleet has a notes file like this:

  • Where things are: the live URL (web address), where the code lives, the exact deploy commands (deploying is copying your app onto your server), and where the backend, a program on your server that answers requests itself, really runs. When Acento's own backend was retired and its job moved to my shared accounts server, its notes said so in bold, so no later session would edit dead code.
  • Decisions (don't undo), with reasons. Baby Signs' notes say "keep US spelling" and "the Deaf-community stance is deliberate, don't strip it", so no session quietly reverses either.
  • Skipped: what you didn't build, and what would change your mind. The next lesson digs into this list.
  • Lessons: each incident becomes a dated LESSON line: symptom, cause, fix. Nearly every gotcha on my medical price tool, Medical Price & Bill Coach, got written down this way, which is why a problem's second appearance took minutes, not hours.

Loud warnings go at the top. The price tool's notes open by warning that production is not my laptop's newest commit, so the next session doesn't ship the wrong code.

What's production?

The live copy of your app that real people use. Touching production means changing anything they'd notice: the server, DNS, payments, their data.

๐Ÿ›‘ Write where a secret lives, never the secret: "the email password is in the server's env file". Notes get pasted, committed and copied, and a password leaks with them. Both files will sit in the folder npx serve hands out (so http://localhost:3000/NOTES.md will work), and a live server does the same: deploy only the files your app needs.

What's an env file?

A file on your server, usually named .env, holding secrets as NAME=value lines. Your program reads it when it starts; it never goes into git or the web folder.

Stale notes are worse than none, because Claude repeats them confidently. After many changes to The List, my explorer for New York's pied-ร -terre tax, its README (a project's front-page notes) still listed removed commands and outdated counts, and its own error message sent stuck users to a command that no longer existed. After big changes, have Claude search the notes and the app's messages for old commands and numbers.

3

Give Claude a memory it reads every session

  1. In Claude Code, type /init and press Enter. It reads your folder, drafts a short CLAUDE.md and asks before writing; approve. The file appears in VS Code's Explorer, the file list on the left (Ctrl+Shift+E; Mac: Cmd+Shift+E). Already have a CLAUDE.md (Windows, from last lesson)? Skip /init.
  2. In the Explorer, hover over your folder's name, click the New File icon, name it NOTES.md and press Enter, paste the text below (replacing YYYY-MM-DD with today's date) and save (Ctrl+S; Mac: Cmd+S).
  3. Ask Claude to wire it in (the first chat below).
# myapp: notes for Claude and for future me

## Read first
- (nothing unusual yet; loud warnings go here)

## Rules for Claude
- Ask before installing anything.
- For any change that touches more than one file, show me a plan before editing.
- Production (the live server, DNS, payments, real users' data): only the exact change I ask for, in my own words, then show me proof it worked.
- If a bug comes back, stop patching: reproduce it outside the app and show me the numbers.

## Where things are
- Live URL: none yet (http://localhost:3000 while I build)
- Code: this folder, in my private GitHub repo
- Deploy: not deployed yet
- Backend: none
- Secrets: none yet. Write WHERE each one lives, never the value.

## Decisions (don't undo)
- YYYY-MM-DD DECISION: dark mode follows the computer's setting, CSS only, because it needs no JavaScript. Revisit if a visitor asks for a toggle.

## Skipped (and what would change my mind)
- A dark mode toggle: build it if a visitor asks for one.

## Lessons (LESSON YYYY-MM-DD: symptom โ†’ cause โ†’ fix)
- (none yet)

EXAMPLE โ€” YOU ASK CLAUDE CODE

Claude Code
YouAdd a last line to CLAUDE.md containing only @NOTES.md. Change nothing else, and show me the diff.
ClaudeAdded @NOTES.md as the last line of CLAUDE.md. The diff is that one green line; nothing else changed.โ†ณ Edit ยท CLAUDE.md

Now prove it loads. Type /exit, then claude for a fresh session (if it asks whether to allow importing NOTES.md, say yes), and ask:

EXAMPLE โ€” YOU ASK CLAUDE CODE (A FRESH SESSION)

Claude Code
YouWithout opening any files: what's my first rule for you, and what did I decide about dark mode?
ClaudeYour first rule: ask before installing anything. Dark mode follows the computer's setting, CSS only, because it needs no JavaScript; revisit it if a visitor asks for a toggle.

What's happening underneath: a new session loads CLAUDE.md, and its @NOTES.md line pulls your notes in before you type a word. Success is that answer without Claude opening NOTES.md first. If it doesn't know, check that the line stands alone, matches the file name, and isn't inside backticks (imports written as code are ignored). Then have Claude commit both files, and keep them short: every line is read every session.

End every session the same way. Your last message: At the end of this session, add what we learned to NOTES.md as LESSON lines: symptom, cause, fix. Read what it wrote; a wrong lesson is worse than none. A line looks like LESSON YYYY-MM-DD: page didn't change after an edit โ†’ the browser showed a saved copy โ†’ hard-reload, and searching VS Code for LESSON (Ctrl+Shift+F; Mac: Cmd+Shift+F) finds them all.

Check its work, every time

Claude's "fixed" is a claim, not a result. Check it, cheapest first: ask for proof ("show me the command you ran and what it printed"), run it yourself in your second terminal, and look at the page in the state you changed. (Check the AI's work turns this into a routine you run before every upload.)

When a bug comes back, measure

The dangerous moment is a bug's second appearance, when Claude offers another plausible fix, then another.

๐Ÿ“ Habooby, my Phoenix dust and air-quality map, had a stubborn bug I called "wind stuck in a box". It survived two guessed fixes (grid size plus retries, then auto-retry plus a fade); both helped, and it kept coming back. Then a bare loop of twelve requests, outside the app, showed the second load already coming back 429. The root cause was the data API's hourly budget: about 11 map loads an hour. The measured fix got that to about 41.

What are an API, a status code and a 429?

An API is a web address that returns data instead of a page. Each answer carries a status code: 200 is OK, 429 is "too many requests, slow down". Free APIs allow so many requests per minute and per hour; use them up and you get 429s until the window resets.

So stop accepting plausible fixes and send this:

EXAMPLE โ€” YOU ASK CLAUDE CODE

Claude Code
YouThis bug came back. Don't patch it. Reproduce it outside the app and show me the numbers.
Clauderepro.mjs calls the API the way the map does, twelve times, and prints each status code. Request 1: 200. Requests 2 to 12: 429. The cause is the request budget, not the drawing code. Run node repro.mjs to see it yourself; I'll rerun it after the fix.โ†ณ Write ยท repro.mjsโ†ณ Bash ยท node repro.mjs

What's happening underneath: "outside the app" means a tiny script making the app's requests and printing what comes back, with no map or page to blame. node runs it: Node runs JavaScript outside the browser. Put the before and after numbers in NOTES.md, and keep the script for next time.

One AI critiques, another builds

For "is this page any good?", ask a reviewer that didn't write the code (a fresh session, or Claude at claude.ai with a screenshot): Review this page as a first-time visitor. Give a numbered list: problem, why it matters, fix, effort (S/M/L). That's small, medium, large. Save the answer as REVIEW.md, have Claude Code ship the cheap wins by number, look at the page after each batch, and park the expensive ones in a STILL DEFERRED list.

๐Ÿ“ The price tool's biggest usability gains came from two passes like this: one headline number, plain-language labels, a real comparison table, negotiation scripts shown by default, accessibility fixes and a data-freshness footer. The big items, like a photo-first bill upload, went on the deferred list instead of being half-built.

Production only when you say so

In manual mode, Claude Code asks before it edits a file or runs a command; if you can't explain a command, ask "what does this do?" before approving. In auto mode, where a second AI model reviews each action instead of you, a safety check also refuses risky production changes (DNS, launching other agents) unless you asked for them explicitly. That's a feature.

What are DNS and an A record?

DNS is the internet's address book. An A record is one line in it, like "myapp.yourdomain.com lives at YOUR_VPS_IP": your app's name under your domain, and your server's IP address, its number on the internet (all three are stand-ins for your own). Get it wrong and your app vanishes.

Both have happened to me: a DNS record went through only once I'd asked for it in so many words, and when a session tried to start another Claude session to hand work over, it was stopped, so it printed the launch command and I ran it myself. Here's the explicit version, once Your own server has connected Claude to your DNS through an MCP (a plug-in that gives Claude Code tools for an outside service):

EXAMPLE โ€” YOU ASK CLAUDE CODE (LATER IN THE COURSE)

Claude Code
YouCreate an A record for myapp pointing at YOUR_VPS_IP in my DNS. Don't touch any other record. Then check it against 1.1.1.1.
ClaudeAdded myapp.yourdomain.com โ†’ YOUR_VPS_IP. nslookup myapp.yourdomain.com 1.1.1.1 now answers YOUR_VPS_IP.โ†ณ hostinger-dns ยท update DNS recordsโ†ณ Bash ยท nslookup myapp.yourdomain.com 1.1.1.1

What's happening underneath: you named the exact change, so it's your decision, not Claude's guess. nslookup asks a public DNS server (1.1.1.1) where a name points. If Claude is blocked, or you'd rather do it yourself, ask for the exact command and run it in a second terminal (the + in VS Code's terminal panel), reading it before you press Enter.

The rule, already in your NOTES.md: on your laptop, Claude edits and runs things with your OK on each command. Production changes only when you've asked for that exact change in your own words, with proof afterwards. And when the same bug comes back, Claude stops patching and measures.

๐Ÿฆฅ Skipped for now: auto mode. Approving each command is slower, but reading them is how you learn what they do. Try it once you can explain what you'd be approving.

Big edits: a script, not fifty find-and-replaces

Sometimes the change is a whole block, like every style rule in a page. Instead of fifty find-and-replace edits that leave half-matched leftovers, ask for a small splice script that replaces everything between two fixed markers (here the <style> and </style> tags) at once, then delete the old patches rather than stacking new ones on top.

๐Ÿ“ First Bites, my first-foods and allergen tracker for babies, had its whole style block rewritten this way: a Node script swapped everything between the <style> tags, about 30 hard-coded colors became named variables (each color defined once, used everywhere), and the old pile of dark-mode patches went. The file is full of emoji, so the edit ran through Node, never PowerShell (the encoding trap from Set up your workshop).

The prompt: Put the new styles in new.css, then write splice.mjs to replace everything between <style> and </style> in index.html with it, stopping with an error if they're missing. Run it and show me the diff. Underneath, it's this small:

import { readFileSync, writeFileSync } from 'node:fs';
const css = readFileSync('new.css', 'utf8');
const html = readFileSync('index.html', 'utf8');
const block = /<style>[\s\S]*?<\/style>/;
if (!block.test(html)) throw new Error('No <style> block found in index.html');
// the () => keeps any $ in your CSS exactly as written
writeFileSync('index.html', html.replace(block, () => '<style>\n' + css + '\n</style>'));
console.log('Style block replaced');

Node reads and writes both files as UTF-8 (the standard text encoding), so emoji survive. Check every screen in light and dark, commit, then delete splice.mjs and new.css.

Later: helpers that research while you work (advanced)

Skip this on a first read; it's for when an app needs lots of content.

What's a subagent?

A helper Claude that your session starts for one slice of work, like researching one topic, in parallel with others. Each starts blank and hands back a report.

  • Strict JSON in, a script to check it. Give each subagent one slice, an exact JSON shape (JSON is data as plain text) and a demand for sources. A small script then drops and prints anything not on your allowed list, refuses duplicates, and splices the rest between /*DATA_START*/ and /*DATA_END*/ markers in your page (Build static-first sets them up). First Bites' 80-food catalog came in this way.
  • A human still reviews. Two Acento slang entries came back with invented pronunciation notes (a "sh" sound in words without the letters that make it); my review caught both. The price tool's hospital lists for five new states came from five subagents in parallel, applied by one script that refused duplicates.
  • Chain long jobs on completion, not a timer. Acento's slang batches (write, build, generate audio, deploy) ran more reliably chained one after another than on the 15-minute timer I first asked for, because the audio step's duration varied.
  • Resume, don't restart. In a multi-step AI run that can resume, edit only the steps that haven't run. Change an early one and everything after it runs, and costs, again.
  • Watch handed-off work in its own visible session (a second terminal, claude, a prompt naming the job), so you can steer or stop it. Keep handoff notes on your laptop, never in the web folder.

Do it with Claude: the prompts

Three prompts carry this lesson. Replace FEATURE with yours, like "a contact form".

Before writing code, give me 2 or 3 options for FEATURE with trade-offs and your recommendation, smallest first.
At the end of this session, add what we learned to NOTES.md as LESSON lines: symptom, cause, fix.
This bug came back. Don't patch it. Reproduce it outside the app and show me the numbers.

What's happening underneath: CLAUDE.md, anything it imports with @, and any memory notes Claude Code keeps for itself load when a session starts. Anything not written there is gone tomorrow.

Your turn

  • In plan mode, get options for dark mode (or another small feature) and pick one. Done when NOTES.md has a dated DECISION line, with a "because", for the option you picked.
  • Build it with the four-part prompt, see it work, read the diff in Source Control, and commit. Done when your commit message shows in git log --oneline.
  • Wire NOTES.md into CLAUDE.md. Done when a fresh session tells you your first rule without opening a file.
  • Ask a fresh session: What are my rules for you about installing things, production, and a bug that comes back? Done when each answer matches your Rules for Claude.
  • End a session with the LESSON prompt. Done when NOTES.md has a dated LESSON line (anything that confused you today counts).

Next, The laziest thing that works: the ladder that stops Claude building a mansion when you asked for a shed, and the SKIPPED and NOT DONE lists for the NOTES.md you made today.