A practical guide to writing specs that turn vague AI prompts into production-quality code β from functional requirements to edge cases, with real before-and-after examples.
Let me paint a picture you've probably lived. You open Claude Code or OpenCode, type a vague prompt like "add a user dashboard with analytics," and hit enter. The agent spins up, writes a bunch of code β it even looks decent at first glance. Then you realize: the data model is wrong, the API endpoints don't match your existing patterns, there's no error handling, and the "analytics" is just a row of four hardcoded numbers.
You spend the next hour correcting, prompting, correcting again. You would have been faster writing it yourself.
Now imagine a different scenario. You spend 15 minutes writing a structured specification, paste it into the agent, and it produces exactly what you wanted β following your conventions, handling edge cases, wired into your existing auth and data layer. One shot.
That's not luck. That's the difference between treating your AI coding agent like a chatbot and treating it like a senior engineer who needs a clear design document β also known as spec-driven development.
Claude Code, OpenCode, Cursor Agent β these tools are incredible when pointed at a well-defined task. They can read your entire codebase, understand your conventions, and produce production-quality code. But they share a fundamental limitation: they don't know what you want unless you tell them, precisely and completely.
When you give an agent a one-liner prompt, you're leaving an enormous amount of ambiguity. The model will fill in the gaps β but it fills them with its own assumptions, which are often generic, incomplete, or just wrong for your context.
A spec closes those gaps. It transforms an open-ended creative writing exercise into a constrained engineering task.
A spec that works for an AI agent is different from a traditional software specification. It doesn't need to be a 20-page document with UML diagrams. It needs to be concise, unambiguous, and machine-actionable.
Here's what a good agent spec includes:
Don't describe the solution. Describe the desired behavior.
β Vague:
Add a login form.
β Spec-driven: Add a login form that:
-
Accepts email and password
-
Calls
POST /api/auth/login -
On success, stores the JWT from the response in
localStorage
underauth_token
and redirects to/dashboard
- On 401, shows a red inline error "Invalid email or password" above the submit button
- Disables the submit button and shows a spinner while the request is in flight
- Validates that email is non-empty and looks like an email before submitting
The second version removes every decision the model would otherwise have to guess.
Tell the agent exactly where things go. AI agents can search a codebase, but they can't read your mind about your preferred architecture.
Create the following files:
frontend/src/pages/Dashboard/DashboardPage.tsx
β the page componentfrontend/src/pages/Dashboard/DashboardStats.tsx
β the stats cards rowfrontend/src/pages/Dashboard/useDashboardData.ts
β custom hook for data fetchingfrontend/src/pages/Dashboard/types.ts
β TypeScript interfaces
This alone can save multiple rounds of "no, put it over here" corrections.
- Use React Query (
useQuery
) for data fetching, notuseEffect
+fetch
-
Follow the existing error handling pattern in
frontend/src/hooks/useApi.ts -
Use the shared
Button
component fromfrontend/src/components/ui/Button.tsx
- Colors: use CSS variables (
`var(--color-primary)`
,`var(--color-error)`
)- Add a skeleton, not a standalone spinner
These constraints are what turn generic code into code that looks like your team wrote it.
AI agents are naturally optimistic. They code the happy path brilliantly. The unhappy path β not so much, unless you tell them about it.
Edge cases to handle:
- Dashboard data is still β show skeleton placeholders
- API returns an error β show error toast with retry button
- User has zero data (new account) β show empty state with CTA
- API response is slow (>3s) β show the skeleton, don't unmount it early
- Browser tab regains focus after >5 minutes β silently refetch data
A single paragraph like this can prevent half a dozen follow-up prompts.
Tests to write:
DashboardPage
renders stat cards when API returns dataDashboardPage
shows skeleton while DashboardPage
shows error toast + retry button on API failureDashboardPage
shows empty state when user has no datauseDashboardData
refetches on window focus after 5+ minutes of inactivity
It's not magic. Here's what's happening when you provide a spec:
Reduced ambiguity β fewer hallucinations. The model doesn't have to invent database schemas, API contracts, or error messages. You've provided them. Every decision you make upfront is a decision the model can't get wrong.
Constrained search space. Modern coding agents work by searching, reading, and reasoning about your codebase. A spec acts as a map β it tells the agent which files to read, which patterns to follow, and which components to reuse. Without it, the agent wastes context window scanning irrelevant code.
Better context utilization. AI agents have limited context windows. When you spend 2,000 tokens on a precise spec, you save 20,000 tokens of back-and-forth corrections. That's a 10x return on the most precious resource in AI-assisted development.
Deterministic output. With a spec, two runs of the same prompt produce similar results. Without one, they can diverge wildly. This matters when you're working on a team β you want predictable, reviewable output.
Before (vague prompt): Add RBAC middleware to the API.
Result: The agent invents its own role model, introduces a new middleware pattern inconsistent with the existing codebase, and misses that the project already has an internal/server/rbac/
package.
After (spec-driven prompt): Add a new RBAC middleware
RequirePermission
tointernal/middleware/rbac.go
that:
- Wraps an
http.Handler
and checks the caller has the required permission- Reads the authenticated user's roles from
internal/apis.Session.Roles
- Accepts a permission string via a functional option:
`RequirePermission("org:write")`
- Looks up the permission-to-role mapping from the existing
internal/server/rbac.Controller
- Returns 403 with
{"error": "insufficient_permissions"}
when unauthorized- Follows the middleware chaining pattern in
internal/middleware/auth.go
- Must be covered by table-driven tests in
internal/middleware/rbac_test.go
testing: authorized, unauthorized, unauthenticated, and admin-override cases
Result: The agent reads the existing RBAC package, middleware patterns, and test conventions, then produces middleware that slots in perfectly β passing tests on the first run.
The difference is stark: 15 minutes of spec writing saves an hour of prompt wrestling.
Write the spec before you open the agent. Force yourself to think through what you actually want. If you can't describe it, the agent can't build it.
Use a template. Create a reusable template with sections: Purpose, Files to Create/Modify, Functional Requirements, Technical Constraints, Edge Cases, Tests. Fill it out in 10β15 minutes.
Be ruthlessly specific. Every time you catch yourself writing a vague word like "handle errors," replace it with exactly what that means: which errors, what message, what UI state.
Reference existing code. Tell the agent which files to use as examples. "Follow the pattern in X" is one of the most powerful lines you can write in a spec.
Iterate the spec, not the code. When the output is wrong, ask yourself: did I specify this? If not, update the spec and re-run. You're building a reusable asset, not just fixing a one-off bug.
We're entering an era where the bottleneck in software development is shifting from writing code to specifying intent. The engineers who thrive will be the ones who can articulate precisely what they want β not the ones who type the fastest.
AI coding agents are force multipliers. But like any multiplier, they amplify what you put in. Feed them ambiguity, and they'll generate a mess β just faster. Feed them a precise spec, and they'll generate production code β in minutes, not days.
The spec isn't overhead. It's leverage.
If you found this useful, follow me for more on AI-assisted development, engineering practices, and building with Claude Code and OpenCode.