Guides
Writing prompts that produce usable code
A code prompt is a specification. Most poor results come from an incomplete specification, not from the model.
Updated 21 August 2026 · 4 min read
When the generated code is wrong, the instinct is to blame the model or reword at random. In the vast majority of cases the problem is elsewhere: the request did not contain the information needed to produce anything other than what came out.
A code prompt is not a question, it is a specification. Everything you leave unsaid, the model decides for you — and it decides by what is statistically most common, which is rarely what you wanted.
The anatomy of a prompt that works
Three elements are almost always missing, and they are what separates throwaway code from usable code.
- Technical context — the stack, the versions, the project's conventions, what already exists. Without it the model invents a plausible environment that is not yours.
- Constraints — what not to do, which libraries are off limits, which values must not be invented. Prohibition has more effect than instruction.
- The success criterion — how you will know it is right. Without one, the model aims for "it compiles", not "it solves my problem".
The same request, two outcomes
Here is the version everybody writes, then the same intent properly specified.
Write me a function to upload a file.Context: Next.js 16 (App Router), strict TypeScript, validation with zod.
Files go to an S3 bucket already configured in lib/storage.ts, which
exposes putObject(key, buffer, contentType).
Write a POST route handler that receives a multipart upload.
Constraints:
- accept only image/png, image/jpeg, image/webp
- reject above 5 MB, without loading the whole file into memory first
- the filename comes from the user: never use it as-is as a storage key
- no new dependency
Done when: a 6 MB file is rejected with a 413, an .exe renamed to .png
is rejected, and two uploads with the same name do not overwrite.The second prompt is five times longer and saves far more time than it costs. Above all, the last line changes the nature of the exchange: you have given the model something to check its own work against.
Four prompts worth reusing
These cover most day-to-day situations. They are deliberately generic: replace what is in brackets.
Getting code audited
You are an attacker trying to exploit this code. Your goal is to find a
concrete way to make it fail or to extract data from it.
For each flaw: the exact input that triggers it, what the attacker gets,
and the minimal fix.
Do not list general best practices. If you find nothing exploitable, say
so rather than padding the answer.
[paste the code]The attacker framing matters: asking "is this code secure?" produces a reassuring list of generalities. Asking for a concrete exploit produces verifiable findings.
Refactoring without breaking
Refactor this file to [specific goal].
Hard constraints:
- observable behaviour must not change, error cases included
- the public signature of exported functions must not change
- no new dependency
Before producing code, list the current behaviours your refactor must
preserve. If one of them looks like a bug, flag it but do not fix it in
the same pass.
[paste the code]Asking for the behaviour list first is the key move: it forces the model to actually read what exists, and it hands you the list of what to retest.
Debugging
Symptom: [what happens]
Expected: [what should happen]
Already ruled out: [what I checked]
Context: [stack, versions, since when]
Give me the three most likely causes, ranked, and for each the fastest
way to confirm or eliminate it.
Do not propose a fix until the cause is identified.
[paste the code and the full error]Stating what you already ruled out skips the first three exchanges where the model offers the obvious hypotheses. Forbidding an immediate fix stops it from masking the symptom.
Writing tests worth having
Write tests for this module using [framework].
Do not test the obvious happy path. Focus on:
- boundary values and empty or missing input
- error cases and what happens when a dependency fails
- the behaviours the rest of the application depends on
For each test, one sentence naming the regression it prevents.
If a behaviour looks untestable as written, say so.
[paste the code]Phrasings that sabotage the result
Some habits of language reliably degrade the output.
- "Do your best" or "however you like" — you are delegating an architecture decision to something that knows neither your constraints nor your future.
- "Simple" and "clean" — these words carry no shared meaning. Say what you want: fewer files, fewer dependencies, fewer abstractions?
- Stacking several requests in one message — the model handles the first well and rushes the rest. One intent per exchange.
- Never saying no — when the first version misses, saying precisely what is wrong beats rewording the request from scratch.
Correct rather than re-ask
Faced with an imperfect result, the temptation is to reword the original request and run it again. This is almost always a mistake: you throw away the parts that were right and draw a fresh sample, with no guarantee it will be better.
Targeted correction works far better. "The size check runs after loading the file into memory, which defeats the protection. Fix only that" produces a precise patch, where rewording produces new code with new faults.
Related reading
Securing AI-generated code
Generated code is functional before it is safe. Here are the recurring flaws, with the signals that let you catch them in review.
Design and UX when the AI draws
Every generated screen is acceptable. Put end to end, they look like nothing. The problem is not aesthetic, it is structural.