What you'll learn

By the end of this you'll know how to write prompts that get consistent, usable code from any AI coding tool — ChatGPT, Claude, Cursor, Copilot Chat, doesn't matter. The principles transfer. You'll understand why constraints are the highest-leverage thing you can add, how to run a verify loop when the first draft isn't right, and how to build a small personal library of prompt templates that work for your stack.

This isn't about using a "better AI." It's about better inputs. The output ceiling is mostly set by the prompt.

Who this is for

  • You've used AI tools to generate code and gotten back something plausible-but-wrong, or plausible-but-not-your-stack
  • You've wondered why the same question gets wildly different results on different days
  • You want a repeatable process instead of just hoping for a useful answer

You can skip this if you already write constraint-rich prompts and have a working verify loop. Jump to Common mistakes if you're getting inconsistent output and can't figure out why.

What is prompt engineering?

Prompt engineering is the practice of writing AI inputs that reliably produce useful outputs. For code, that mostly means: specifying your environment, adding explicit constraints, and asking for exactly what you want instead of the general version of it.

Plain English: the AI predicts a likely continuation of your message. More specific messages constrain the prediction toward something you can actually use.

Simple idea: a prompt is a specification. The more complete the spec, the closer the first draft lands to what you actually need.

Prerequisites

  • Familiarity with any AI coding tool: ChatGPT, Claude, Cursor, or Copilot Chat
  • Basic coding experience — examples use TypeScript but the patterns work in any language
  • No special setup required; these are workflow techniques, not library configurations

Key terms

Constraint — a specific restriction in your prompt: language, framework, allowed dependencies, return type, output format. Each constraint narrows what the AI can generate. More constraints, closer output.

Verify loop — the cycle of generate → test or run → refine with a targeted follow-up → repeat. Most good outputs come from turn two or three, not turn one.

System prompt / Custom Instructions — a persistent background instruction that applies to every conversation. Worth configuring with your stack preferences so you don't re-establish context each session.

Scope — the size of the task you're asking about. Narrow scope (one function, one behavior) gets more consistent output than broad scope (a whole feature, a whole module).

Prompt template — a reusable prompt structure you've tested and saved. The setup step below walks through building a few.

Setup from zero

Step 1 — Set persistent context

If you're using ChatGPT or Claude, configure Custom Instructions (ChatGPT) or an equivalent system prompt to carry your stack defaults. Something like:

I write TypeScript and React. I use Next.js App Router.
Prefer Server Components. Don't add "use client" unless I ask.
Use Tailwind for styles. No CSS-in-JS.
Production-quality code, no placeholder comments.
Flag edge cases I might have missed.

This applies to every session. You won't need to re-establish the environment every time you open a new chat.

If you're using Cursor, put equivalent instructions in your .cursorrules file. If you're on Copilot Chat, there's no persistent system prompt yet — keep a short snippet you paste at the top of a new conversation.

Step 2 — Build a constraint template

The single most useful habit: before you send a prompt, add a constraints block. Here's a before/after for a real example.

Before (too vague):

Write a function to fetch user data from an API.

After (constrained):

Write a TypeScript async function that fetches user data from a REST API.
- Uses the native fetch API, no axios or other HTTP libraries
- Accepts a userId: string parameter
- Returns Promise<User | null> — return null on 404, throw on other errors
- Type the response as User: { id: string; name: string; email: string }
- Handle network errors with a try/catch

Same task. Five constraints. The second version produces code you can drop in without rewriting half of it.

Step 3 — The verify loop

Don't expect perfection on turn one. The loop:

  1. Write the constrained prompt, get a response
  2. Run the code or read it carefully for type errors and edge cases
  3. Come back with a specific follow-up: "This throws when the API returns a 500 — handle that by returning null and logging the status code"
  4. Repeat until it passes

The follow-up prompt is where a lot of developers stop too early. That second message — "when I run this I get [specific error]" — is often the most valuable exchange in the conversation.

The mental model

Prompt engineering is version control for your intent. Your first draft of a prompt is like a first draft of code — it communicates the general idea but misses specifics. Iteration tightens it. The AI can't know your production constraints, your existing types, your error handling conventions. You have to put those in the prompt.

And the verify loop is your test suite. You wouldn't ship code without running tests; don't accept AI output without testing it. Run it, break it, bring the error back as a targeted follow-up.

Step-by-step

A refactoring prompt

Refactor this function to use async/await instead of Promise chains.
Don't change the return type, error handling behavior, or function signature.
Only change the internal implementation.

[paste function]

The "only change X" clause matters for refactoring prompts. Without it, the AI might clean up other things while it's in there — things you didn't ask for and may not want.

A code review prompt

Review this TypeScript function for bugs and edge cases.
Don't rewrite it — just list issues.
Specifically:
- Any cases where this throws unexpectedly?
- Is the return type accurate for every code path?
- Any obvious performance issues for arrays over 1000 items?

[paste function]

Breaking the question into three specific sub-questions gets targeted answers instead of generic style feedback. Ask for issues, not a rewrite, so you stay in control of the actual code.

Working examples

Before/after prompt comparison

A prompt that gets vague output:

Help me write an API route.

The same prompt with constraints:

Write a Next.js App Router route handler at app/api/newsletter/route.ts.
- Accepts POST with body { email: string }
- Validates the email with a simple regex — no dependencies
- Returns 400 with { error: "Invalid email" } if validation fails
- Returns 200 with { ok: true } on success
- Log the email to console for now (no actual service integration)

The second version has a path, a request shape, a validation rule, explicit error shapes, and a clear scope boundary. That last part matters — it stops the AI from hallucinating a Mailchimp integration you didn't ask for.

Little tip: always include the expected return shape in your prompt. "Returns { ok: true }" or "Returns Promise<User | null>" is one line that saves you from debugging a response format that doesn't match what your frontend expects.

When the first draft misses

Say the generated code uses fetch but doesn't check response.ok before parsing the JSON. Don't start a new conversation:

The previous version doesn't check response.ok before parsing the JSON.
Update it to: if response.ok is false, log the status code and return null.
Keep everything else the same.

"Keep everything else the same" is the key phrase. Without it, the AI may take the opportunity to restructure other parts of the function.

Little tip: keep a note file with your best follow-up phrases. "Keep everything else the same." "Don't change the function signature." "Add this without touching the existing error handling." These phrases are reusable across any prompt and any tool.

Patterns / when to use

Use prompt engineering when:
- You need code in a specific stack and constraints matter
- You're debugging with structured error context
- You want a review focused on specific concerns, not general style
- You're iterating on the same function across multiple turns

Don't expect prompt engineering to fix:
- Asking about a library that shipped very recently — training data has a cutoff
- Very long contexts — even well-constrained prompts degrade when you paste 400 lines
- Security-sensitive logic — generate a draft if you want, but verify line by line regardless of how good the prompt was

Common mistakes

Vague scope — "improve my component" will produce generic feedback. "Check whether this component re-renders when the parent prop items changes, and tell me why, without rewriting it" asks one specific question and gets a specific answer.

One-shot expectations — turn one rarely produces shipping code. The verify loop exists because refinement is normal. Two or three turns for a non-trivial function is expected, not a sign the tool is broken.

Over-constraining on the wrong things — "write clean code" doesn't narrow anything. "Use early returns instead of nested if/else" does.

Not specifying existing types — if you have a User type already defined in your codebase, paste it in the prompt. The AI will invent its own version otherwise, and now you're reconciling two type shapes.

Troubleshooting

Getting different answers to identical prompts — AI models have temperature (randomness). Add "Be consistent with the existing code style in this function" or, for deterministic formats, ask for a structured output (JSON, a checklist) where format constraints reduce variation.

The fix introduced a new bug — that's normal in turn-two iteration. Include the new error in your follow-up: "The fix for the 404 case now causes a type error on line 14 — here's the error: [paste]. Fix only the type issue, don't change the 404 handling."

The AI keeps adding things you didn't ask for — add an explicit "Do not add anything else" or "Only make the change I described." Sounds overly restrictive but it's genuinely necessary for tight-scope prompts.

Checklist

  • [ ] Persistent context configured (Custom Instructions, .cursorrules, or a snippet you paste)
  • [ ] Every coding prompt includes at least: language, framework, return type, and dependency constraints
  • [ ] Return shape specified explicitly for any function that produces output
  • [ ] Verify loop used — first draft tested before it ships
  • [ ] Specific follow-up messages reference only the exact issue being fixed
  • [ ] "Keep everything else the same" added to targeted refinement prompts
  • [ ] Sensitive logic reviewed line by line regardless of prompt quality
  • [ ] A note file started with reusable constraint phrases for your stack

Practice task

Take a function from your current project — something real, 20–40 lines. Write a constrained prompt from scratch: specify the language, describe what you want to change, add the return type, and add at least two explicit "do not" constraints. Send it to ChatGPT or Claude. If the first response doesn't compile or has a type error, run one iteration of the verify loop — bring the error back as a specific follow-up. Write down what changed between your initial prompt and the follow-up that made the second response better. That's your template for the next time.

FAQ

Does prompt engineering work the same on all AI tools?
The core principles transfer — constraints, verify loops, scope — but specific behavior varies. Cursor has rules files that persist context across sessions. Claude tends to follow explicit constraints more literally than ChatGPT does. Copilot Chat has less conversation memory. The strategies here work on all of them; the setup steps differ.

How long should a prompt be?
Long enough to carry the constraints, short enough to stay focused. For a code-generation task, 5–15 lines is typical. For a debug request, you're adding the code snippet and error text, so it gets longer — that's fine. What doesn't scale well: prompts that describe the entire application context when you only need help with one function.

Should I save my best prompts somewhere?
Yes. A simple text file or a notes app. You'll reuse constraint patterns across tools and sessions. "Return null on 404, throw on other errors" and "Only modify this function, don't touch the ones around it" are prompts you'll write a dozen times — template them once.

What to learn next

  • ChatGPT for coding — the companion guide on using ChatGPT specifically, including the debug loop format and safe pasting habits
  • Cursor AI complete guide — how to move prompt engineering into a persistent rules file that shapes every session on your codebase
  • GitHub Copilot workflow — applying these patterns in the Copilot completions and chat context inside VS Code
  • [How to use ChatGPT for coding](/ai/tutorials/chatgpt-for-coding)
  • [Cursor AI complete guide](/ai/tutorials/cursor-ai-complete-guide)
  • [GitHub Copilot workflow that ships](/ai/tutorials/github-copilot-workflow)

Takeaways

The output quality of any AI coding tool scales with the specificity of the prompt. Add constraints — language, framework, return type, what not to touch. Run the verify loop — test the first draft, bring errors back as specific follow-ups. Build a small library of prompt templates that work for your stack so you're not starting from scratch each time.

If you remember only one thing: the constraint sentence changes the output more than anything else. Language, framework, return type, what to avoid — one line of constraints turns a vague request into a targeted spec.