VCS
VIBE CODE STUDIO

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.

  1. 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.
  2. Constraints — what not to do, which libraries are off limits, which values must not be invented. Prohibition has more effect than instruction.
  3. 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.
What this yields: a plausible function, in some arbitrary stack
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 same intent, specified

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

All guides