Blog / AI/ML

Prompt Engineering for Developers: Turning an Idea Into a Working Build Spec

The gap between "build me a login system" and code you can actually ship isn't a prompting trick — it's a missing specification. A concrete, side-by-side framework for closing that gap, with two full worked examples.

Two developers ask an AI assistant to "build me a login system." One gets a plausible-looking Express route that stores passwords in plain text and leaks whether an email address exists in the system through its error messages. The other, using the exact same underlying model, gets something close to production-ready on the first pass. The difference between them was never a clever prompting trick — it was that the second developer had already made the decisions the first one left for the model to guess at, and guessing wrong on a handful of them is exactly what happened.

Why "just describe what you want" reliably falls short

A vague prompt forces an assistant to silently make dozens of decisions on your behalf — what shape the data takes, what happens on the unhappy path, which of several reasonable interpretations of "login system" you actually meant — and it will get some of them wrong, not because the model is bad, but because it genuinely cannot read your mind about the constraints living only in your head. A real build spec doesn't make the model smarter. It just removes the guessing, by answering the decisions that actually mattered before you ever ask for code.

The four questions a real spec answers

What is the exact input, and what is the exact output — not "takes user data," but the literal shape: field names, types, one concrete example of what a request and a response actually look like. What should happen on the unhappy path — empty input, a network failure, a value outside the expected range — decided explicitly rather than left implicit for the model to invent on the spot. What does this need to fit into that already exists — an existing schema, an existing API contract, an existing code style — named directly, so the assistant extends what's there instead of quietly reinventing a convention you already have. And what's explicitly out of scope for this pass, which is nearly as useful to state as what's in scope, because it stops an eager assistant from building three adjacent features you didn't ask for and now have to review anyway.

Worked example one: the login endpoint

Weak version: "Build me a login system."

Build-spec version, answering all four questions above before a single line of code exists:

Build a login endpoint for an Express + Postgres API.

- POST /login accepts { email, password } as JSON
- Look up the user by email; if not found, return 401 with
  { error: "invalid credentials" } — do not reveal whether the
  email exists in the system
- Compare the password against the stored hash using bcrypt
- On success, return a signed JWT (15 min expiry) and also set it
  as an HttpOnly, Secure, SameSite=Strict cookie
- On any failure, return the same generic 401 as above
- Do not implement registration or password reset in this pass

Notice what changed isn't the tone or the cleverness of the wording — it's that every decision an assistant would otherwise have had to guess at is now already made. The security-relevant one — not revealing whether an email exists in the system, which prevents an attacker from enumerating valid accounts — is exactly the kind of decision a vague prompt leaves entirely up to chance, and it's a real, common vulnerability in login endpoints built without that instruction.

Worked example two: a data pipeline, where the stakes are different

The same framework applies well outside of web APIs. Say the real task is: "process a folder of CSV exports and load them into a reporting table." A vague version of that prompt leaves an assistant guessing at what might be the most consequential decisions in the entire task — what happens to a row with a malformed date, whether re-running the pipeline on the same file twice should create duplicate rows or safely no-op, and how large a file the process needs to handle without running out of memory.

Build a script that ingests daily CSV exports into a `daily_sales`
reporting table in Postgres.

- Input: CSV files matching `sales_YYYY-MM-DD.csv`, columns are
  date, region, product_id, units_sold, revenue_cents
- Skip and log (don't crash on) any row with a missing or
  malformed date or a negative units_sold
- Running the script twice on the same file must not create
  duplicate rows — upsert on (date, region, product_id)
- Files may be up to 2GB; stream the file, don't load it fully
  into memory
- Do not build a scheduler or a UI for this pass — this is a
  script invoked manually or by an external cron job

This example is worth including specifically because the highest-value decisions here — the duplicate-row behavior and the memory constraint — are exactly the kind of thing that's invisible in a quick manual test (a small test file works fine either way) and only shows up as a real, expensive problem in production, once the pipeline runs against an actual 2GB file or gets accidentally triggered twice on the same day's data. Deciding these upfront, in the spec, is dramatically cheaper than discovering them as an incident later.

Iterating instead of restarting from scratch

Once you have a first draft, the fastest path forward is almost always a small, targeted follow-up — "handle the case where the JWT secret environment variable is missing at startup" — rather than re-describing the entire feature from zero each time something needs adjusting. Treat the conversation as an ongoing, living spec you're refining together, one decision at a time, not a single all-or-nothing request you either got right or didn't.

Common anti-patterns worth naming

Over-specifying implementation details you don't actually care about is its own failure mode, just a quieter one than under-specifying — telling an assistant exactly which variable names to use or which specific library function to call, when what you actually cared about was the behavior, wastes your own time writing the spec and can lock in a worse implementation choice than the one the assistant would have picked on its own. Specify behavior and constraints; leave implementation details unspecified unless you have an actual reason to care about them.

Another common one: writing a spec, getting a result that mostly works, and accepting it without re-reading the original spec side by side with what was actually built. It's easy for a subtle requirement — "don't reveal whether the email exists," say — to quietly not make it into the final implementation even when it was clearly stated, especially across a longer back-and-forth conversation. Checking the finished code against the original spec, line by line, is a cheap habit that catches this reliably.

Writing a genuinely good build spec is a real, transferable engineering skill in its own right — arguably more transferable than memorizing any one language's syntax, because it's the identical skill whether you're briefing an AI assistant or handing a task to a junior teammate on your team.

The two-minute habit worth building

Before opening a chat with an assistant, spend two minutes writing the spec as though you were handing it to a capable engineer who has genuinely never seen your codebase before. If you find you can't actually write that spec yet — you're not sure what the unhappy path should do, or what the exact output shape needs to be — that's real, useful information too. It means the idea isn't concrete enough to build yet, and no amount of clever prompting was going to fix that; only more thinking was.

Want to build something like this?

NebuCoders is free to join — no application, no cost.

Read next