All Guides
prompt engineeringaillmpromptingproductivitycopilotclaude

Prompt Engineering for Developers

A deep-dive into writing prompts that actually work: context, roles, chain-of-thought, multi-turn workflows, system prompts, and the mental model behind why models respond the way they do.

Ryan VerWey
2026-04-02
20 min read

Why Most Developer Prompts Fail

You have probably noticed that two developers can use the exact same model and get wildly different results. One gets production-ready code on the first try. The other gets hallucinated APIs, missing error handling, and a function that works in the happy path only.

The difference is almost never the model. It is the prompt.

Models are not search engines. They are prediction engines. They generate the most statistically likely continuation of your input. If your input is vague, the continuation will be "generic developer task." If your input is precise and contextual, the continuation will be "exactly what you described."

This guide covers the mental model, the core framework, and the advanced patterns that separate engineers who get real value from AI from engineers who gave up after a week.


The Mental Model: You Are Writing a Spec

Stop thinking of prompts as questions. Start thinking of them as specifications.

When you write a function specification for a junior engineer on your team, you include:

  • What the function does
  • What it takes as input
  • What it returns
  • What errors it must handle
  • What constraints it must follow
  • What "done" looks like

A good prompt is the same thing. The model is a very fast, very knowledgeable contractor who will do exactly what you specify and nothing more. If you spec it well, you get great work. If you spec it poorly, you get a first draft that misses half the requirements.


The CRAFT Framework

Every effective prompt nails five things:

Context → Role → Action → Format → Tone

Context:  What is the situation? What stack, what problem space, what constraints?
Role:     Who should the model be? What expertise should it bring?
Action:   What exactly do you want? Be as specific as you would be in a ticket.
Format:   How should the output be structured? Code block, JSON, numbered list?
Tone:     How should it communicate? Terse and technical, or explained step by step?

Not every prompt needs all five explicitly. But the best prompts cover all five implicitly.


1. Context: The Highest Leverage Element

The single most common failure is under-providing context. The model has no access to your codebase, your schema, your previous conversations (unless you provide them), or your mental model of the problem. You have to provide all of it.

Under-specified (what most developers write):

Fix this bug in my code.

Well-specified:

I'm building a Next.js 15 app with the App Router, TypeScript in strict mode,
and Supabase for auth and database.

This is a server action that updates a user's profile. The bug is that it throws
"Cannot read properties of undefined (reading 'id')" when the user is not
authenticated. The action is being called from a form in a protected route,
but the middleware check isn't catching unauthenticated requests to this endpoint.

Code:
```ts
export async function updateProfile(formData: FormData) {
  const supabase = createServerClient()
  const { data: { user } } = await supabase.auth.getUser()
  const name = formData.get('name') as string
  await supabase.from('profiles').update({ name }).eq('id', user.id)
}

Error: TypeError: Cannot read properties of undefined (reading 'id') at updateProfile (app/actions/profile.ts:6:55)

Expected behavior: return a typed error object { error: 'Unauthorized' } when the user is null, so the client can redirect to /login.

Constraints:

  • Do not throw, return errors as typed values
  • Keep the Supabase client pattern I'm already using
  • TypeScript strict mode, no any

The second prompt gets you a correct, production-grade fix. The first gets you a guess.

### What context to always include

- Framework and version
- Language and TypeScript config (strict mode matters)
- Libraries in use (and relevant versions if behavior has changed)
- The full relevant code, not abbreviated snippets
- The exact error with stack trace
- What you expect versus what you got
- Any constraints on the solution

---

## 2. Role: Activating the Right Mode

Prefixing a prompt with a role is not a magic trick. It works because models internalize patterns from writing in specific contexts. Saying "you are a security auditor" activates a different pattern of attention than "you are a code generator."

Use roles that match the kind of output you want:

| Role | When to use |
|------|------------|
| `Senior TypeScript engineer` | Production-quality code with strict types |
| `Security auditor` | Surfaces vulnerabilities, injection risks, auth flaws |
| `Code reviewer` | Bugs, performance issues, anti-patterns |
| `Technical writer` | Clear, structured documentation |
| `Rubber duck` | Explain the problem back to me so I understand it better |
| `System architect` | Trade-off analysis, architecture decisions |
| `Principal engineer doing a code review for a junior` | Detailed explanations alongside corrections |

A role works best when it is specific and combined with a behavioral contract:

You are a senior TypeScript engineer. Your code is always:

  • Strict mode compatible with no implicit any
  • Error-handled at every async boundary
  • Built around the existing patterns in the codebase I will show you
  • Minimal: no extra abstractions unless clearly necessary

Here is the codebase context: [paste relevant files]


---

## 3. Action: Specificity Eliminates Ambiguity

Vague requests produce vague answers. The action in your prompt should be as specific as a well-written GitHub issue.

| Vague | Specific |
|-------|---------|
| "Write a function" | "Write a TypeScript async function `getUserById(id: string): Promise<User \| null>` that queries Supabase, handles the case where the user does not exist by returning null, and throws a typed `DatabaseError` on query failure" |
| "Review my code" | "Review for memory leaks, missing null checks, and improper error handling. Output as a table: Issue \| File:Line \| Severity (Critical/Warning/Suggestion) \| Fix" |
| "Explain this" | "Explain why this recursive function causes a stack overflow for inputs over 10,000. Explain it for a developer who understands JavaScript but has never debugged deep recursion before" |
| "Improve this" | "Improve the readability of this function. Do not change its behavior or type signatures. Explain each change you make and why" |

The test: if you read your action statement back and could imagine two different reasonable outputs, it is not specific enough.

---

## 4. Format: Constrain the Output Shape

Telling the model how to format its response is as important as telling it what to do. Without a format constraint, you get essay-style prose around the thing you actually wanted.

Common format directives:

Respond ONLY with the code. No explanation before or after.

Respond with:

  1. Root cause (2-3 sentences, plain English)
  2. Fixed code in a TypeScript code block
  3. Bullet list of what changed and why
  4. Two edge cases I should add tests for

Format the output as a JSON array of objects with keys:

  • file: string
  • line: number
  • issue: string
  • severity: "critical" | "warning" | "suggestion"
  • fix: string

Use a markdown table. Columns: Pattern | When to use | When NOT to use


Format constraints also save tokens. If you only need the code, asking for only the code halves the response length and keeps the thread focused.

---

## 5. Chain-of-Thought: Make the Model Reason Before Acting

For complex problems, explicitly asking the model to reason before answering significantly improves output quality. This is chain-of-thought prompting.

Without it, the model jumps straight to generation. With it, the model surfaces its reasoning, and you can catch errors in logic before they become errors in code.

**Pattern:**

Before writing any code, reason through this problem:

  1. What are the edge cases this function needs to handle?
  2. What data structure best fits this access pattern and why?
  3. What's the time complexity of your proposed approach?
  4. Are there any race conditions or async ordering issues?

After reasoning through those, write the implementation.


**Why it works:** The reasoning tokens force the model to compress relevant knowledge before generating. Mistakes in reasoning are visible and correctable. Mistakes buried in code generation are not.

Use chain-of-thought for:

- Architecture decisions
- Algorithm selection
- Debugging complex multi-system issues
- Anything where "why" matters as much as "what"

---

## 6. Multi-Turn Prompting: Iteration is the Workflow

A single prompt is rarely the whole workflow. Professional use of AI tools looks like a structured conversation, not a one-shot query.

**Iteration pattern:**

Turn 1: "Here is the problem. What are the top 3 approaches and their trade-offs?" Turn 2: "Go with approach 2. Here are additional constraints I forgot to mention: [...]" Turn 3: "Good. Now add error handling for the case where the API is unavailable." Turn 4: "Extract the retry logic into a standalone utility. It will be reused." Turn 5: "Write unit tests for the retry utility covering exponential backoff and max retries."


Each turn builds on the last. The model retains the context of the conversation (until the window fills). You are programming incrementally, not prompting once and hoping.

**Correction pattern:**

When the model gets something wrong, be precise about what was wrong rather than repeating the entire prompt:

That's close but the error handling is wrong. You're swallowing the error and returning undefined, but I need it to re-throw as a NetworkError class that extends Error with a statusCode: number property. Keep everything else the same.


Imprecise corrections ("that's wrong, try again") often produce a different wrong answer. Precise corrections produce targeted fixes.

---

## 7. System Prompts: Persistent Context for Tool Integrations

If you use Cursor, GitHub Copilot, Claude via API, or any tool with system prompt support, a well-crafted system prompt is the highest-leverage thing you can invest in.

The system prompt sets the model's persistent identity and behavioral contracts for every conversation in that context.

**A production-grade system prompt for a coding assistant:**

You are a senior full-stack engineer working inside this codebase.

Stack: Next.js 15 App Router, TypeScript strict mode, Tailwind CSS v4, Supabase for auth and database, Zod for validation.

Code standards:

  • No any types
  • All async functions handle errors explicitly; never swallow them
  • Server Components by default; "use client" only when required
  • Return typed error values from server actions rather than throwing
  • Zod schemas for all external input, including form data and API responses

When reviewing or modifying existing code:

  • Preserve existing patterns and naming conventions
  • Do not refactor unrelated code unless explicitly asked
  • Flag potential issues you notice but do not fix them silently

When you are not certain about something, say so before guessing. Ask one clarifying question before writing code for complex requirements.


The investment in a system prompt pays off on every subsequent interaction.

---

## 8. Prompt Patterns for Common Developer Tasks

### Debugging

I have a bug. Let me give you everything you need to diagnose it.

Expected: [what should happen] Actual: [what's happening] Environment: [Node 22, TypeScript 5.5, Next.js 15] Frequency: [always / sometimes / specific conditions]

Code:

[paste the minimal reproducing code]

Error:

[paste the full error with stack trace]

Give me your top 3 hypotheses in order of likelihood, then tell me how to verify each one before suggesting a fix.


### Code Generation

Write a TypeScript function with this exact signature:

async function [name]([params]): Promise<[return type]>

Requirements:

  • [requirement 1]
  • [requirement 2]
  • [edge case handling]

Constraints:

  • Strict TypeScript, no any
  • No external dependencies beyond [list allowed]
  • Must be unit-testable (inject dependencies, do not hardcode them)
  • Follow the async/await pattern already in this codebase

After the function, include a usage example showing the happy path and one error path.


### Code Review

Review this code as if you were a principal engineer reviewing a PR from a mid-level developer.

Output format: | Issue | Location | Severity | Fix | Where severity is: Critical / Warning / Suggestion / Praise

Focus on:

  • Correctness and logic errors
  • Security vulnerabilities (injection, auth, data exposure)
  • Performance issues (N+1 queries, unnecessary re-renders, memory leaks)
  • Type safety (implicit any, missing null checks)
  • Error handling gaps

Do not comment on style unless it creates a real readability problem.

Code: [paste code]


### Refactoring

Refactor this code. Goals in priority order:

  1. Remove duplication
  2. Improve type safety
  3. Improve readability

Hard constraints:

  • All existing behavior must be identical
  • All public function signatures must stay the same
  • Do not extract abstractions for code that appears only twice

For each change:

  • Quote the before
  • Show the after
  • Explain the reason in one sentence

[paste code]


### Documentation

Write a JSDoc comment for this function. Include:

  • @description — one sentence, plain English, what it does
  • @param — one entry per parameter with type and what it represents
  • @returns — what it returns and in what conditions
  • @throws — what errors it can throw and when
  • @example — one realistic usage example

Be concise. A junior developer should understand how to call this function without reading the implementation.

[paste function]

---

## 9. Model Selection for Developers

The right model matters as much as the right prompt for complex work.

| Task | Best choices |
|------|-------------|
| Hard reasoning and debugging | Claude Sonnet 4.5, o3, Gemini 2.5 Pro |
| Code generation (large files) | Claude Sonnet 4.5, Gemini 2.5 Pro, DeepSeek V3 |
| Fast completions and autocomplete | o4-mini, Gemini 2.0 Flash |
| Very long document or codebase analysis | Gemini 2.5 Pro (1M context window) |
| Self-hosted or privacy-sensitive | DeepSeek V3, Qwen 2.5 Coder |

Higher reasoning capability costs more tokens and latency. For quick completions and autocomplete, a fast smaller model beats a slow large one. For architectural decisions and complex debugging, pay for the reasoning.

---

## 10. The Habits That Separate Good from Great

**Save prompts that work.** When you write a prompt that produces reliable results, save it. A personal prompt library is one of the highest-leverage tools you can build for yourself. That is what the SLOPSTACK prompt library is for.

**Iterate, do not restart.** When the first response is close but not right, give precise corrective feedback rather than rewriting the entire prompt. Treat the conversation as a pair programming session.

**Give the model an out.** Explicitly tell the model it is okay to say it does not know, ask for clarification before guessing, or list assumptions. Models are trained to produce confident-sounding output. Giving them permission to be uncertain produces more honest and accurate responses.

**Read the output critically.** AI-generated code will look correct. Check it the same way you would check a PR from a developer you trust but have not met. Verify it compiles. Verify the error handling actually handles errors. Verify the types are actually safe.

**Update your system prompts as the codebase evolves.** A system prompt that describes your stack six months ago will produce code that is inconsistent with your current patterns. Treat it like documentation: keep it current.

---

## Common Mistakes

| Mistake | Fix |
|---------|-----|
| Asking multiple questions in one prompt | One question per prompt, or explicitly structure them as numbered items |
| Omitting the relevant code | Always include the full relevant code, not paraphrased descriptions |
| Not specifying the tech stack | Always include framework, language, and relevant library versions |
| Accepting the first answer | Iterate. Ask for alternatives, edge cases, and improvements |
| Treating AI output as reviewed code | Read every line the same way you would review a PR |
| Giant unformatted prompts | Use markdown structure: headers, code blocks, numbered lists |
| Asking the model to guess missing requirements | Specify fully, or ask the model to list its assumptions before generating |

---

## Summary

Prompt engineering is not a collection of tricks. It is a discipline of clear specification. The developers who get the most out of AI tools are the ones who already know how to write good requirements, precise bug reports, and clear technical specs. Prompting is the same skill applied to a different medium.

The CRAFT framework (Context, Role, Action, Format, Tone) gives you a checklist. Chain-of-thought forces reasoning before generation. Multi-turn workflows let you build incrementally. System prompts give you persistent context across every interaction. And saving your best prompts compounds the investment over time.

The model is the tool. The prompt is the spec. Write better specs.


## Why Prompt Engineering Matters

The model is only half the equation. A mediocre prompt to a great model often produces worse output than a well-crafted prompt to a smaller model. Prompt engineering is the highest-leverage skill a developer can learn right now.

> "The best prompt is the one that gives the model exactly the context it needs to be right."

## The Core Framework: CRAFT

**C**ontext → **R**ole → **A**ction → **F**ormat → **T**one

Every effective prompt nails these five elements:

Context: What is the situation? What codebase, what problem space? Role: Who should the model be? (Senior engineer, code reviewer, etc.) Action: What exactly do you want it to do? Be specific. Format: How should the output be structured? (JSON, markdown, code) Tone: How should it communicate? (Technical, beginner-friendly, concise)


## 1. Context is King

The number one mistake developers make is under-providing context. The model doesn't have access to your codebase, your schema, or your requirements unless you tell it.

**Bad:**

Fix this bug in my code.


**Good:**

I'm building a Next.js 15 app with PostgreSQL via Prisma. This is a server action that updates a user's profile. The bug is that it throws "Cannot read properties of undefined (reading 'id')" when the user is not authenticated.

Here's the code: [paste code]

Here's the error with full stack trace: [paste error]

I expect it to return a 401 error with the message "Unauthorized" instead of crashing.


## 2. Assign a Role

Telling the model *who it is* dramatically affects output quality. Roles activate latent training patterns.

You are a senior TypeScript engineer who writes production-grade code with strict null safety, comprehensive error handling, and zero any types.


Effective developer roles:
- **Senior full-stack engineer** → Clean, idiomatic, production-ready code
- **Security auditor** → Surfaces vulnerabilities and attack vectors
- **Code reviewer** → Finds bugs, performance issues, and anti-patterns
- **Technical writer** → Clear, concise documentation
- **System architect** → High-level design, trade-off analysis

## 3. Be Specific About the Action

Vague requests get vague answers. The more specific your action, the better the output.

| Vague | Specific |
|-------|---------|
| "Write a function" | "Write a TypeScript async function that fetches user data from `/api/users/:id`, handles 404 and 500 errors with custom error types, and returns `Promise<User \| null>`" |
| "Review my code" | "Review for memory leaks, missing null checks, and improper error handling. List issues by severity: Critical, Warning, Suggestion." |
| "Explain this" | "Explain why this causes a stack overflow, in plain English for a junior developer who knows JavaScript but hasn't encountered recursion bugs before." |

## 4. Specify Output Format

Constrain the format of the response for maximum usefulness.

Respond with:

  1. A brief explanation of the root cause (2-3 sentences)
  2. The corrected code in a TypeScript code block
  3. A list of what changed and why, in bullet points
  4. One or two edge cases I should test

Common format directives:
- `Respond ONLY with code, no explanation.`
- `Format as a markdown table with columns: Issue | Severity | Fix`
- `Use numbered steps.`
- `Keep your response under 200 words.`
- `Include a before/after comparison.`

## 5. Advanced: Chain-of-Thought

For complex reasoning, explicitly ask the model to think step by step.

Before writing any code, reason through the problem:

  1. What are the edge cases?
  2. What data structures should I use and why?
  3. What's the time and space complexity?

Then write the implementation.


This forces the model to reason before acting, significantly improving output quality on hard problems.

## 6. Working with Code: Key Patterns

### Debugging Pattern

I have a bug. Let's debug it systematically.

Expected behavior: [what should happen] Actual behavior: [what's happening] Environment: [Node 22, TypeScript 5.5, etc.]

Code:

[paste code]

Error (if any):

[paste error]

First, list your top 3 hypotheses for what's wrong. Then suggest how to verify each one.


### Code Generation Pattern

Write a [function/class/hook] that:

  • Does X
  • Handles Y edge case
  • Returns Z
  • Works with [framework/library version]

Constraints:

  • TypeScript with strict mode
  • No external dependencies beyond [list what's okay]
  • Must be testable (don't hardcode dependencies)

Include a basic usage example at the bottom.


### Refactoring Pattern

Refactor this code. Goals:

  1. Improve readability
  2. Remove duplication
  3. Improve type safety

Constraints:

  • Keep all existing behavior identical
  • Don't change the function signatures
  • Explain every significant change

[paste code]


## 7. System Prompts vs User Prompts

**System prompts** set persistent context and persona - use them in tools like Cursor, Continue, or the API.

**User prompts** contain the specific task - these change with every conversation.

A solid system prompt for a coding assistant:

You are an expert full-stack engineer. Your code is:

  • TypeScript-first with strict mode
  • Clean and readable over clever
  • Error-handled at every boundary
  • Free of magic numbers and unexplained logic

When you see a problem, you fix the root cause, not just the symptom. You always ask clarifying questions before writing code for complex requirements.


## 8. Temperature & Model Selection

The right model for the job matters:

| Task | Recommended |
|------|------------|
| Complex reasoning & debugging | Claude 3.7 Sonnet, o3 |
| Code generation | Claude 3.5 Sonnet, Gemini 2.5 Pro, DeepSeek V3 |
| Quick completions | Gemini 2.0 Flash, o4-mini |
| Long document analysis | Gemini 2.5 Pro (1M context) |
| Self-hosted / offline | DeepSeek V3, Qwen 2.5 Coder |

## Common Mistakes to Avoid

- **Asking multiple questions in one prompt** → Break into separate prompts
- **Not providing code** → Always include the relevant code
- **Not specifying language/framework** → Always include your tech stack
- **Accepting the first answer** → Iterate! Ask for improvements, alternatives, edge cases
- **Giant unformatted prompts** → Use markdown structure in your prompts

## Putting It All Together

The best prompt engineering habit: **save your best prompts**. Use a tool like SLOPSTACK's prompt library to build your personal collection of prompts that work reliably for your workflow.

When you find a prompt that consistently produces great output, save it, iterate on it, and reuse it.
Ryan VerWey

Written by

Ryan VerWey

Ryan VerWey is a full-stack developer building tools and writing practical guides for working developers.