Building Apps With AI
Build working software before you can write it, without fooling yourself.
- Level
- Nothing assumed
- Lessons
- 87
- Reading time
- 713 min
- Price
- Free, no sign-up to read
People who cannot really write code are shipping working software with Cursor, Claude Code, Replit, Lovable and Bolt. This course teaches that skill honestly: what the tools actually do, how to describe what you want, how to read code well enough to trust it, how to recover when the model breaks your app, git and deploying for people who have never done either, what it really costs, the security holes AI code has by default, and the point where you must learn to code properly or stop.
Opens after the Building With AI exam
Sign in, finish that course, and pass its exam. You can read this syllabus meanwhile.
Go to Building With AIModule 1
The machine, and what the model can see of it
Before the first prompt, four things have to be true: you know which kind of tool you are holding, you know what a web app is made of, you know what the model can and cannot see of your project, and your computer can actually run the thing. This block sets all four up, including the free routes for a phone or a laptop that will not hold a build.
By the end you can
Choose between an inline completion tool, an editor agent and a prompt-to-app platform for a given job, name the four parts of a running web app and which of them the model can observe, and get a project running on your own machine or a free hosted one without asking anybody for help
- 1What these tools actually doThese tools generate plausible code quickly; deciding whether it is correct is still entirely your job.
- 2What a web app is actually made ofEvery web app is a browser, a server, a database and the network between them; the browser is a stranger's machine, so nothing enforced there is enforced at all.
- 3The context window is the whole memoryThe model knows only what is in this turn's context window, so most surprising behaviour is a file it never read rather than a mistake it made.
- 4What the model cannot see, and how to give it eyesMost of what the model gets wrong is something you can see and it cannot, so the fix is usually to paste the error, the data or the screen rather than to argue.
- 5Getting a machine that can actually buildA working setup is Node via a version manager, git, an editor and WSL on Windows; if you cannot install any of it, Codespaces or Replit is a real path rather than a compromise.
- 6The terminal, for people who have been avoiding itA terminal is either waiting for you or busy; read the last ten lines of any failure, and check the path in any command that deletes or force-pushes before agreeing to it.
- 7Choosing a stack, and then stoppingPick a mainstream stack because models write it better, write the choice down where your tool reads it, and only reconsider when you hit a specific thing you cannot do.
- 8The rules file, and what belongs in itA rules file under two pages that names your stack, your file layout, your build command and the mistakes you have already made is the only text guaranteed to reach the model every turn.
- 9Autonomy, and where to keep your handSet autonomy by how cheaply you can verify the result, commit before any long run, and read the test-file diff whenever an agent reports that everything passes.
Module 2
Saying what you want, precisely enough
A model will build whatever you describe and silently invent answers to everything you left out. Those inventions are next week's bugs. This block turns a sentence into something a stranger could build from: the data underneath, the screens on top, the slice you can check in two minutes, the acceptance checks written before the code, and the fence that stops an agent redecorating files you did not mention.
By the end you can
Turn a one-line feature idea into a specification a stranger could build from — entities, actions, rules, failure paths and written acceptance checks — and name in advance the decisions a model will invent for you if you leave them unstated
- 10Describing what you wantDescribe what must be true and what happens when it fails; the happy path writes itself.
- 11The data model comes before the screensSettle the nouns, their fields and how they relate before any screen exists, because a wrong data model outlives every other mistake in the project.
- 12Slicing a feature so every step is checkableCut vertically through every layer in slices you can check in two minutes, prove the plumbing with a trivial walking skeleton first, and commit at each working step.
- 13Writing the acceptance checks before the codeWrite five given-when-then checks before the code — happy path, boundary, refusal, permission and recovery — because a check written afterwards only describes what the code already does.
- 14Sketching the screens, including the empty onesEvery data screen has four states and generated code builds only the loaded one, so write the empty, loading and error states into the request along with the phone-width layout.
- 15Naming things so you can still find themFix one word per concept and one naming convention per layer, because in code you did not write the names are the only index you have.
- 16Examples beat descriptionsPoint at an existing file, paste three messy real rows, and write the exact output you expect — every ambiguity resolved by an example is one the model cannot invent an answer to.
- 17Fencing the change so it stays where you put itState which files may change and what must not be touched, ask for a file-by-file plan before code, and check the result with git diff --stat because a fence is a filter rather than an enforcement.
- 18Making the model ask before it guessesA model fills unstated gaps silently, so require questions before code on anything expensive to reverse and keep the answers in the repository as the start of a specification.
Module 3
Reading and checking what came back
Generated code arrives faster than anybody can read it, and the standard for accepting it is usually that it looked right. This block replaces that with a method: read the diff rather than the file, learn just enough HTML, JavaScript and SQL to audit rather than to author, use the browser's own instruments, and settle any claim with the smallest experiment that could disprove it.
By the end you can
Audit a change you did not write — read its diff, find the query behind a page, inspect the running app with browser devtools, and design a two-minute experiment whose result would distinguish a working feature from one that only appears to work
- 19Reading code you did not writeRead code to answer one question at a time, and confirm claims by using the app rather than searching the text.
- 20Reading a diff properlyRead the removed lines before the added ones and start with git diff --stat, because unexpected files and swapped conditions are visible before you read any code.
- 21The shape of a project you did not lay outPackage.json, the entry point and the routes folder map any project; keep components free of database access as a folder rule so you can check it by looking.
- 22Enough HTML and CSS to audit the screenCheck that a form input's name matches what the server reads, that controls are real buttons and labels, and that the layout survives 360 pixels and the Tab key.
- 23Enough JavaScript to review a changeA missing await, a swallowed catch, an `any`, and a `use client` file that touches secrets account for most reviewable JavaScript defects in generated code.
- 24Reading the query behind the pageFind the WHERE clause, confirm the user's identity in it came from the session, and check for a LIMIT and for queries running inside a loop.
- 25The browser devtools as an x-rayOpen the console for errors nobody read, the network panel to see whether a request happened and what came back, and throttle to Slow 4G before you show anybody the app.
- 26The smallest experiment that settles itPredict the result before you run it, change one thing, and break the code you think is responsible to confirm the behaviour actually depends on it.
- 27Printing what you cannot seeNumbered prints of the actual values at each boundary locate a failure faster than reasoning about the code, and a console.log of a request body is how apps accidentally write passwords into their logs.
Module 4
When it breaks, and getting back
Everybody building this way meets the same hour: something errors, the model changes files, a different error appears, and two hours later the app will not start. That hour is a procedure problem rather than a prompting problem. This block gives you the procedure — read the error, classify the failure, find the change that caused it, verify that a fix is a fix, and know when to abandon a conversation or a feature.
By the end you can
Recover a broken app on purpose — read a stack trace to the line in your own code, classify a failure by where it happened, find the exact change that introduced it, distinguish a real fix from a silenced symptom, and write a question a stranger could answer
- 28When the model breaks your appTwo failed fixes means stop, revert to a working state, and reproduce the bug before asking again.
- 29The anatomy of an error messageRead the error type, the message, and the first stack line naming your own file; the frame below the crash usually holds the mistake that produced the bad value.
- 30Classifying the failure before you fix itPlace a failure in the browser, the server, the database or the network before diagnosing it, because a silent wrong answer and a 500 need completely different first moves.
- 31Finding the change that broke itAnswer when it last worked with a commit, then halve the range instead of reading every diff; handing over the culprit commit's diff beats describing the symptom.
- 32Dependency trouble, and the lock fileCommit the lock file and reach for npm ci before deleting it, because removing the lock to fix a problem silently changes several hundred versions at once.
- 33It works here and not thereFilename case, missing environment variables and the gap between the dev server and a real build cause most first deployments to fail; running npm run build and npm run start locally catches nearly all of it.
- 34Was it actually fixed, or just silencedUndo the fix and confirm the bug returns; if it does not, the change you are about to keep is not what altered the behaviour.
- 35Knowing when to restart the conversationA long session accumulates failed theories that compete with the useful context, so ask for a written handover and restart at a clean commit rather than persuading a conversation out of its own history.
- 36Asking a human, and getting an answerA question with the goal, the steps, the exact error text, the expected result and what you already tried gets answered; building the minimal reproduction solves the problem about half the time before you ask.
Module 5
Save points, and shipping to a stranger
Everything before this block happened on one machine with no memory of yesterday. This block gives the project a history and an address: what a commit really holds, the three different ways back, branches so an agent's bad afternoon never touches the working app, a remote so the record is yours, and then the shipping half — the variable prefix that leaks secrets to every visitor, why a failed build looks like nothing changed, and a domain with its padlock.
By the end you can
Keep a project in git well enough to undo any hour — stage part of a change, restore one file, revert a pushed commit, recover from a bad reset, branch for an agent's work and merge it back — and put the app on a free host with a real domain, environment variables scoped per environment, and a version stamp that says which commit is live
- 37Save points: git when you have never used itA commit made while the app works is the only thing that makes a bad hour cost an hour.
- 38What a commit actually isA commit is a snapshot of whatever you staged, not a list of edits, so staging is where you decide which of today's changes belong together.
- 39Three ways back, and the undo for undoRestore one file, revert one commit, or reset everything — they are different operations, and once a commit has been pushed only revert is safe.
- 40Branches for the risky changeA branch is a movable name for a commit, so an agent's failed afternoon on a branch costs one delete command and main never saw it.
- 41GitHub, and what a remote is forA remote is another copy of the history that only accepts commits building on what it already has, which is what a rejected push is telling you.
- 42Merge conflicts without panicGit only stops where two sides changed the same lines; the merges that hurt are the clean ones, so build after every merge.
- 43Letting the agent use git, within limitsAn agent that wants a clean working tree will discard your uncommitted work to get one, so commit before every session and forbid the four destructive commands in writing.
- 44Deploying so a stranger can open itProduction differs from your laptop in configuration, case sensitivity and users, so deploy on day two, not week three.
- 45Environment variables, and the prefix that leaksA variable with a public prefix is copied into the JavaScript every visitor downloads, so a secret with that prefix is not a secret.
- 46Which version is liveA failed build leaves the last good deployment serving, so 'I pushed and nothing changed' is usually a build log you have not read yet.
- 47A domain of your ownDNS answers are cached for the record's TTL, so a change reaches different devices at different times and 'works on my phone, not my laptop' is normal for an hour.
Module 6
The parts that persist: data, accounts and files
A prototype keeps its data in a variable and its users in a single hard-coded account, and both vanish the moment a second person or a second process appears. This block adds the parts that outlive a request: a database chosen on purpose, migrations that change it without losing anything, the four columns every table needs, sign-in from a library rather than from scratch, sessions as the only source of identity, uploads in object storage, a backup you have actually restored, and an arrangement that keeps the production credential out of the agent's reach.
By the end you can
Add a database, sign-in and file uploads to an app without losing anybody's data — choose between SQLite, hosted Postgres and hosted SQLite for a given host, read a generated migration and run it as a deploy step, explain why a session cookie is the only evidence of identity, keep uploads out of the repository and the database, restore a backup into a fresh database, and diagnose a page that runs one query per row
- 48Where the data goes when the app restartsAnything held in a variable or written to the app's own disk vanishes on a serverless host, so the first real decision is which database, and SQLite is the honest place to start until you deploy.
- 49The schema, and the migration that changes itA migration is a numbered, forward-only file that changes the tables, and the one command that skips migrations by resetting the database is the one that deletes everybody's data.
- 50The columns every table needsA generated schema gives each table exactly the columns the feature mentioned, and the owner, timestamps and deletion marker it left out are the ones the next feature and the first security fix will need.
- 51Seed data, and the empty state nobody testedDevelopment with a full database hides the empty state every new user meets first, so seed on purpose and test with zero, one and ten thousand rows.
- 52Sign-in, without writing it yourselfAuthentication is the one feature where the model's from-scratch answer is dangerous even when it works, so use a library or a hosted provider and learn the six-step OAuth dance well enough to debug the redirect error.
- 53Sessions, cookies, and who the server thinks you areEvery request is a stranger until the server looks up the session cookie, so the user's identity must come from that lookup and never from anything the request body says about itself.
- 54Files and uploads, and where they must not goUploaded files belong in object storage with only their address in the database, because the app's own disk is temporary and a table of blobs slows every query that touches it.
- 55Backups, and restoring one before you need toA backup you have never restored is a hope, and the free tier's restore window is measured in hours, so make a dump you own and restore it into a fresh database once.
- 56The database and the agentAn agent that can read your .env can reach production, so give it the schema and a development database, and never the connection string that holds real people's rows.
- 57When the data gets big enough to noticeA page that was instant at a hundred rows and takes thirty seconds at a hundred thousand is usually one query per row or a missing index, and both are found by counting queries and reading the plan.
Module 7
Putting a model inside your own app
Once an app works, the next request is always an AI feature, and the generated version calls the provider from the browser with the key in public. This block builds the feature the way it survives users: the call from your own server with a timeout and streaming, the prompt as a versioned file, user text treated as untrusted, answers in a declared shape validated in code, three caps on cost, a fallback for the provider's bad hour, retrieval that never crosses the user boundary, a twenty-item set that says whether a change helped, and the honest label on top.
By the end you can
Add an AI feature to an app that survives contact with users — call the model only from a server route, cap what one user can spend in three places, validate structured output before anything reads it, explain why prompt injection is defeated by limiting consequences rather than by wording, scope retrieval to the session before ranking, and run a fixed set of inputs before every prompt or model change
- 58The key never goes to the browserAnything the browser has, every visitor has, so the model is called from your own server route and the browser only ever talks to you.
- 59The first call from your own serverA model call is an HTTP request that can take twenty seconds, so it needs a timeout, an error path for each status, and streaming before it feels like a feature rather than a hang.
- 60The prompt is a file, not a stringA prompt is code that changes behaviour, so it lives in a versioned file, is stamped into every log line it produced, and is changed on a branch like anything else.
- 61User text inside a prompt is untrustedA model cannot tell instructions from data, so the defence against injected text is not a better prompt but limiting what the model's output is allowed to cause.
- 62Structured answers you can renderAsk for JSON in a declared shape, validate it in code before anything reads it, retry once with the error, and have a plan for the second failure, because the model will invent a fifth value for a four-value field.
- 63Capping what one user can cost youCost is tokens in plus tokens out, per call, per user, per day, and it needs three caps in three places: a hard limit at the provider, a per-user count in your own table, and a maximum length on every request.
- 64When the model is down, slow or emptyThe provider will have a bad hour, so the feature degrades on its own — a bounded retry, a second model behind the same interface, or an honest unavailable message — and the rest of the app carries on.
- 65Giving the model your own dataThe model knows nothing about your database; the app fetches the rows this user may see, puts them in the prompt, and asks — and an embedding is how it finds the rows that are about the question rather than the ones that share its words.
- 66Is the feature actually goodTwenty fixed inputs with a written expectation each, run by a script before every prompt or model change, is the smallest thing that turns 'it seems better' into a number you can compare.
- 67Telling users it is AI, and what you send about themEvery AI feature says it is one where it appears, sends the provider no more than the feature needs, and is built knowing that the rules on disclosure and data differ by country and are still moving.
Module 8
The security holes AI code has by default
A model writes code that works when you try it, and you try it as yourself, logged in, doing the intended thing. This block is about the unintended thing: the check that only tests ownership when three kinds of person see the row, the key in the built bundle and in a three-week-old commit, injection through the shell and the server's own HTTP client, the body spread into an update, the one prop that turns off escaping, the sign-up flood, the upload that is a document, the admin page that was merely unlinked, and what a model's review of its own code can and cannot find.
By the end you can
Audit a generated app for the holes it has by default — write a single authorisation function and test it with three accounts, find a secret in the built bundle or the git history and revoke it in the right order, locate string-built SQL, shell commands and server-side fetches of user URLs, refuse unknown fields with a strict schema, sanitise at the one prop that injects HTML, limit sign-up and expensive routes with a bot check and per-account counters, detect an upload by its bytes, gate admin routes against an allowlist the app cannot write, and run a per-route checklist plus a free scanner before each release
- 68The security holes AI code has by defaultBeing logged in is not the same as being allowed; every query must be scoped to the user the session says you are.
- 69Authorisation beyond ownershipMost rows are seen by more than one kind of person, so the check is a single named function that answers 'may this user do this action to this row', called on every route and tested with three accounts rather than two.
- 70Secrets in the built bundle, and in the historySearch the built output and the whole git history for keys, because a secret is leaked by the build that inlines it and the commit that once held it, not only by the file you can see today.
- 71Injection in three places: the query, the shell, the URLInjection is user text reaching an interpreter as code, and there are three interpreters in a typical app — the database, the shell and the server's own HTTP client — each with the same fix: pass data as data, never by building the command from strings.
- 72Validate at the edge, and the field nobody meant to acceptA route that spreads the request body into an update accepts every column the caller chooses to send, so every route declares the fields it takes with a schema and ignores the rest.
- 73Cross-site scripting, and the prop with the honest nameFrameworks escape text by default, so XSS in a generated app arrives through the one prop that turns it off — usually to render markdown or a model's output — and the fix is to sanitise at that exact point.
- 74Rate limits, and the sign-up floodAny route a script can call will be called ten thousand times, so the expensive and the account-creating routes get a bot check before sign-in and a per-account limit after it, and the per-address limit is set knowing that one address can be a whole city.
- 75Uploads that runA file's extension and declared type are claims the uploader made, so the server reads the first bytes to decide what it is, serves it from a different origin, and never lets the uploader choose the name on disk.
- 76The admin page nobody protectedAn admin route is protected by a check in the route itself against an allowlist the app cannot write to, not by being unlinked, not by a flag a form can set, and not only by middleware.
- 77Asking the model to audit itself, and what it cannot seeA model reviewing code finds the holes that are visible in the code — a missing check, a raw query, a spread body — and cannot find the ones that live in what the code does not say, so the review is a per-route checklist plus a free scanner plus a person when the stakes are high.
Module 9
What it costs, running it, and where this stops
A shipped app has a bill, a set of free tiers that end by three different mechanisms, users who write 'it doesn't work', a privacy notice it probably lacks, and a release routine it needs. This block runs the app for other people at a known cost — and then names the point at which building without understanding stops working, what to learn to get past it, in what order, from which free resources, and how to use the model as a tutor rather than a builder. It ends with one real thing, built for one real person, and the checklist that is the whole course on a page.
By the end you can
Run a small app for other people at a known monthly cost — say for each service whether it pauses, slows or bills at its limit, catch what throws, what is down and what is merely wrong with three different watchers, turn 'it doesn't work' into a log line and a regression item, write the privacy notice and the deletion path a public site needs, release with an additive migration and a flag, hand the project to a developer with a README that runs — and recognise the specific point at which to learn to code, with a sixty-hour path to get there
- 78What it actually costsUsage-based costs need a hard cap at the provider and a per-user limit in your own code, set before launch.
- 79The free tiers, and exactly where they endEach free tier ends by one of three mechanisms — it pauses, it slows, or it bills — and reading which one applies to each service you depend on is what decides whether a busy day is a dead site or an invoice.
- 80Knowing it broke before users tell youError tracking catches what throws, uptime pings catch what is down, and a report button catches what is wrong — three different failures, and a shipped app needs all three because each is blind to the other two.
- 81The first user complaintA complaint becomes solvable when it can be joined to a log line, so every error shown to a user carries a reference id, every log line carries the same id, and the fix ends with a new item in the regression set.
- 82The legal minimum of a public siteA public site that stores anything about a person needs a privacy notice that lists the third parties the data reaches, a real deletion path that includes the backups, and an honest acknowledgement that the rules differ by the user's country and are not settled.
- 83Shipping changes without breaking what worksA release is a routine, not an event: additive migration first, code second, cleanup in a later release, a flag for anything you might need to turn off, and fifteen minutes watching the error tracker before you walk away.
- 84Handing it to a real developerA developer joining an AI-built project needs a README that runs, the rules file, the test scripts and the complaint list — and when they say the code should be rewritten, the working app and its users are evidence that deserves a specific reason.
- 85Where you have to learn to codeThe limit is not app size or traffic; it is nobody holding an accurate model of how the system works.
- 86The learning path from here, with the model as tutorSixty hours in the right order — errors, one request end to end, your own data, fifty lines unaided, ten tests — with the model told to explain and question rather than write, turns a builder who guesses into one who reads.
- 87Building one real thingThe course is finished when a stranger has used something you built for a week, and the checklist of what 'built' means is the whole course in one page.