How to prompt an AI to build a Next.js app step by step (budget app example, with checkpoints that catch bugs) A developer documented a 25-prompt sequence for building a Next.js 16.4.2 and Tailwind v4 expense-tracker app with an AI coding assistant, running each prompt literally and recording where it broke. The failures traced to prompts that left design choices open, such as an unqualified create-next-app invocation and a seed-data call placed only on the home page, which left the /expenses category dropdown empty in a fresh browser. The fix was to specify exact CLI flags, file structure and import aliases, and to make seed functions idempotent and callable from every page, with a visible checkpoint such as `npx tsc --noEmit` after each step. "Build me a budget app" works for about ten minutes. Then the AI coding assistant fills in every decision you didn't make, and you're debugging choices you never saw. The fix is boring but it works. Split the app into small prompts, make each prompt decide everything up front, and end each step with a checkpoint you can see. This post walks through that structure with a real example: a Next.js 16 + Tailwind v4 expense tracker that stores everything in localStorage expenses, categories, monthly budgets, a summary page . We wrote a 25-prompt sequence for that app and ran it as written, one prompt at a time, reading each prompt literally the way an assistant would. Most of it worked. Where it broke, the cause wasn't hard code but a sentence that left a choice open. Those are below. Versions: create-next-app 16.4.2 Next.js 16.4.2, React 19.3, Tailwind CSS v4 , Node 20. Each prompt in the sequence has four parts: Here's a step in that shape: Implement lib/storage.ts with typed helpers: loadExpenses , saveExpenses , loadCategories , saveCategories , loadBudgets , saveBudgets . Use the keys vibe-budget:expenses , vibe-budget:categories , vibe-budget:budgets . Every helper must return early if typeof window === "undefined" . If data is missing, invalid JSON, or not an array, return and do not crash. CHECKPOINT: npx tsc --noEmit shows no errors. The vague version "use distinct keys prefixed with vibe-budget: ", "handle SSR safely" also works, but leaves decisions that later prompts have to guess. Ahead of the steps, put a few house rules that apply to every prompt. For this app there are three: "use client" . localStorage or call new Date while rendering. Start state empty and load it in useEffect or an event handler. YYYY-MM-DD and months as YYYY-MM . Never use toISOString for "today." Rule 2 avoids a group of Next.js 16 build and lint failures, covered in the previous post https://dev.to/shipinanhour/nextjs-16-tailwind-v4-5-traps-ai-coding-assistants-keep-hitting-and-the-prompts-that-fix-them-3ioc . Rule 3 gets its own section below. For each one: the original wording, what went wrong when it was read literally, and the rewrite. What happened: create-next-app asks a series of questions ESLint or Biome, React Compiler, src/ , import alias, AGENTS.md… . The prompt answered none of them. Rewrite: give the exact command. npx create-next-app@latest vibe-budget --ts --tailwind --eslint --app --use-npm --yes With 16.4.2 this runs without asking anything. Pass the explicit flags as well as --yes . According to the CLI docs, --yes means "use previous preferences or defaults," so on a machine with saved preferences, --yes alone might not give you the defaults you expect. src/ or the project's app root " What happened: the default scaffold has no src/ . So where does lib/ go, and is the import @/lib/... or @/src/lib/... ? Rewrite: "Use this structure at the project root no src/ folder … Import with the @/ alias, e.g. import { loadExpenses } from "@/lib/storage" ." Showing one import line settles the question. ensureSeedData , called once from the client layout or home page" This was the clearest example of a prompt that looked fine and caused a bug three steps later. What happened: the root layout is a Server Component, so "home page" was the only place it could go. Then, in a fresh browser, opening /expenses first showed an empty category dropdown , because the seed data only ran on / . The add-expense form was useless. Rewrite: make the seed function safe to call repeatedly, and require every page to call it: // lib/seed.ts export function ensureSeedData : void { if typeof window === "undefined" return; if loadCategories .length 0 return; // safe to call on every page saveCategories DEFAULTS.map name, color = { id: uid , name, color } ; } Rule for all later prompts: every page calls ensureSeedData inside its useEffect before loading categories, so the app works no matter which page the user opens first. The checkpoint changed too. The original was "you can add an expense and see it after refresh," which passes as long as you started on the home page. The new one targets the bug directly: CHECKPOINT: In a fresh private window, open /expenses directly. The category select lists 8 categories. What happened: the type said "ISO string," and the form used