ai newsDo., 22. Okt.
Eilmeldung1Password für Agents: KI-Assistenten loggen sich ein, ohne Passwörter zu sehen

How to prompt an AI to build a Next.js app step by step (budget app example, with checkpoints that catch bugs)

"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 wit

dev.toDonnerstag 15:02 · 22.10.8 Min. Lesezeit
Bild: dev.to

"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:

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:

Rule 2 avoids a group of Next.js 16 build and lint failures, covered in the previous post. 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.

npx create-next-app@latest vibe-budget --ts --tailwind --eslint --app --use-npm --yes Enter fullscreen mode Exit fullscreen mode 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.

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.

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 }))); } Enter fullscreen mode Exit fullscreen mode 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 <input type="date">. Those don't match. A date input's value is always yyyy-mm-dd (MDN), while "ISO string" pushes the AI toward new Date().toISOString(), which is in UTC. Quick check in Node:

$ TZ = Asia/Taipei node -e 'const d=new Date(2026,9,19,7,30); console.log(d.toISOString().slice(0,10))' 2026-10-18 # it's Oct 19 locally $ TZ = America/Los_Angeles node -e 'const d=new Date(2026,9,19,20,30); console.log(d.toISOString().slice(0,10))' 2026-10-20 # it's Oct 19 locally Enter fullscreen mode Exit fullscreen mode The reverse also happens: new Date("2026-10-19") is parsed as UTC midnight, so in New York it's the evening of Oct 18.

Rewrite: name the format and the helper:

// lib/dates.ts export function todayISO (): string { const d = new Date (); const m = String ( d. getMonth () + 1 ). padStart ( 2, " 0 " ); const day = String ( d. getDate ()). padStart ( 2, " 0 " ); return ` ${ d. getFullYear ()} - ${ m } - ${ day } `; } export const currentMonth = () => todayISO (). slice ( 0, 7 ); Enter fullscreen mode Exit fullscreen mode Then compare dates as strings: expense.date.startsWith(month). For month arithmetic, use new Date(year, monthIndex + delta, 1). That form works in local time, and overflow carries into the next year, so December + 1 becomes January.

What happened: either choice works until you look at the edge cases. Can "Other" itself be deleted? What happens to the deleted category's budgets? In the test run, leftover budgets for a deleted category stayed in storage and inflated the Summary page's "sum of limits."

Delete: confirm first, then move every expense in that category to "Other", delete any budgets for that category, then delete the category. "Other" itself cannot be deleted. Put this rule in a code comment.

"Pick one" is fine in a conversation. In a prompt sequence it isn't, because later prompts can't know which option the AI picked.

Every vibe-coded money form ends up with NaN or $12.3456 somewhere. Don't just ask the AI to "validate amounts." Give it the function signature and the rules:

Create lib/money.ts with formatMoney(amount: number) and parseAmount(input: string): number | null that accepts only plain positive numbers like 12 or 12.5, rounds to 2 decimals, and returns null otherwise. Use it in every form.

export const round2 = ( n: number ) => Math. round ( n * 100 ) / 100; export function parseAmount ( input: string ): number | null { if (! /^ \s * \d + (\.\d + )?\s *$/. test ( input )) return null; const n = round2 ( Number ( input )); return n > 0? n: null; } Enter fullscreen mode Exit fullscreen mode "12.345" → 12.35. "abc", "", "-5", "0", "1,000", "$12" → null. Store numbers; format only when displaying.

What happened: on the 1st through the 6th of a month, "the last 7 days" goes back into the previous month. The demo data then ends up partly outside the month the budgets and summary are showing.

Rewrite: "Keep every date inside the current month (if today is early in the month, use the 1st instead of going back into last month)."

Two smaller ones: a prompt titled "Home redirect" whose body asked for a home page (title and body must agree), and a checkpoint saying "all four/five routes render" (give the exact number).

A checkpoint that only proves the happy path is decoration:

The most important checkpoint is the final one. The original QA prompt was a manual click-through and never ran the build. In the test run, npm run lint and npm run build were exactly where the app broke. So the final prompt now starts with:

Run npm run lint and npm run build and fix every error. Then walk through: add/edit/delete an expense; filters; category add/rename/delete; budgets and bar colors; summary and month buttons; export/import; a 375px-wide window; a hard refresh on every page.

After the fixes, the finished app passed tsc, lint (0 problems), the build, and a 32-step Playwright smoke test.

Assistants don't remember earlier prompts reliably. Cursor's docs put it simply: "Large language models don't retain memory between completions." So don't just put the house rules at the top of prompt 1. Put them in a file the assistant loads on every request:

Keep it short: the house rules, the folder/alias rule, the storage-key prefix, and "finish with npm run lint && npm run build."

Don't start the next prompt. Paste one narrow fix prompt that names the symptom, check it, then re-run the checkpoint that failed before moving on. For the empty dropdown, this was enough:

Category select is empty when I open /expenses directly in a fresh browser. Call ensureSeedData() at the start of every page's useEffect, before loading categories. Don't change types or storage keys.

One more lesson: a fix prompt can sound right and still miss. In the test run, the generic "hydration / render Loading… until mounted" rescue did not fix a new Date() build failure. The real cause was a config default. Run the checkpoint again after every fix, even when the fix sounds obviously right.

If the assistant tries to switch stacks or "just add a database" to get unstuck, say no and keep it on the plan.

If you want to try this on the budget app, the first five prompts of the sequence (scaffold, folder structure and types, storage layer, seed data, app shell, with their checkpoints) are available as a free sample of the first five prompts.

Quelle: dev.to (dev.to). Original öffnen