Back to course overview
Module 5Adding AI 18 min

Lab: The AI feature

Ship both AI features end to end: the key handled right, the draft route with its contract prompt, streaming into the reply box, tones wired, failures rehearsed.

The lab this course is named for. By the end, the disabled button from Module 3 keeps its promise: real drafts, streamed in, in the user's chosen tone, with the by-hand path intact when the magic hiccups.

  1. 1Set up the account and the hygiene: create an API account (any major provider — the course examples use Claude), set a spending cap, get a key, and do the env ritual: .env.local (verify it's in .gitignore — actually look), .env.example committed with a placeholder — one catch: create-next-app's .gitignore ignores .env*, which silently skips .env.example too, so add the line !.env.example to .gitignore first, then confirm with git ls-files | grep env (it should list exactly one file). Run git log --all -S 'sk-ant-' to prove no key has ever touched your history — no output means clean; if something matches, add -p to see it (an innocent word is fine; a real key means asking the assistant for the history-scrub procedure immediately). Make that paranoia a habit.
  2. 2Write the prompt as product: lib/prompts.ts with the draft-reply prompt — tone variants (friendly / professional / brief) as small deltas on one base, the reply-to-their-actual-points instruction, length cap, sign-off from settings, and the [NEEDS INFO: …] escape. Test it in a chat window against your five seed messages before wiring anything — prompt bugs are cheapest in the playground.
  3. 3Persist Settings, then build `/api/ai/draft`: first, give Module 3's tone picker a database home — ask the assistant to add a one-row settings table (tone, sign_off) to lib/db.ts and wire the Settings page to save and load it (a new table, so create-if-missing handles it — no surgery on existing tables needed). Then the route: POST, validates message_id, loads the message + tone + sign-off from the database (server-side truth, not client-supplied text — subtle but important: the browser tells you which message, never what it says), calls the model streaming, pipes the stream back. Test the door directly first, per the Module 4 habit — Console fetch, not the address bar.
  4. 4Wire the frontend: Draft button → streaming into the editable textarea, drafting state, cancel-on-navigate, was_ai_drafted: true when a draft gets sent. Then add Summarize for bodies over ~600 characters: summary column, check-cache-first route, TL;DR line above the body. (Adding a column to an existing table is the one thing create-if-missing can't do — ask the assistant for the one-line ALTER TABLE, or delete your dev database file and re-seed. Either is fine in dev; knowing why is the lesson.) Two features, one afternoon — because the second one reused every pattern from the first. Feel that; it's the compounding this course sells.
  5. 5Rehearse the failures on purpose: set the key to garbage (config error path — gentle message, hand-path works), simulate a timeout (ask the assistant to temporarily set the route's timeout to one millisecond, click Draft, watch the transient-error path fire, then restore it), and click Draft on the emoji-only seed message (does the [NEEDS INFO] escape fire, and does the UI treat it as a draft needing human help rather than sending it?). Commit: ai features + failure paths. The failure rehearsal is the difference between shipping a feature and shipping a demo.
Lab 5 problem set

Prompt-as-product reps: write a fourth tone ('apologetic — for when we messed up') and its test cases; find the planted flaw in a rival draft prompt (it instructs the model to invent order details the message never mentioned); price out your app's monthly AI bill at 10, 1,000, and 50,000 messages and identify which number makes caching mandatory; and write the three error messages for a different AI feature (image alt-text generation) without reusing this lab's copy verbatim.