Most developers treat prompts like Google searches — type something vague and hope for the best. That works for quick questions, but the moment you need reliable, structured, production-quality output from an LLM, you need actual technique. Prompt engineering isn't magic. It's a set of patterns that make language models predictable and useful.
I've spent the last year building applications on top of OpenAI and Anthropic APIs. The difference between a naive prompt and a well-engineered one isn't subtle — it's the difference between an output you can parse and ship versus one you have to throw away and retry. Here's what actually works.
What Is Prompt Engineering?
Prompt engineering is the practice of designing inputs to language models that consistently produce useful, accurate, and well-formatted outputs. It's not about tricking the model — it's about communicating clearly with a system that interprets instructions literally.
Think of it like writing a really good ticket for a contractor. The more specific and structured your instructions, the closer the result matches what you actually want. Vague instructions produce vague results. Precise instructions with examples produce precise results.
The discipline involves understanding how models process text, what context they need, how to structure requests for complex tasks, and how to constrain outputs to specific formats. It's a skill that compounds — once you understand the core patterns, you apply them everywhere.
Why Prompt Engineering Matters for Developers
If you're building any application that calls an LLM API, prompt engineering directly affects your product quality, costs, and reliability:
- Accuracy: Well-structured prompts reduce hallucinations and off-topic responses significantly
- Cost: Efficient prompts use fewer tokens — at scale, this saves thousands per month
- Latency: Shorter, focused prompts mean faster responses for your users
- Reliability: Consistent outputs mean fewer retries and edge cases in your parsing logic
- Maintainability: Prompts are code. Well-structured prompts are easier to iterate on and version
The gap between developers who understand prompt engineering and those who don't is growing. It's becoming as fundamental as knowing how to write good SQL queries or design REST APIs.
Core Prompt Engineering Techniques
These are the techniques I use daily. Each one solves a specific problem and they combine naturally for complex tasks.
Zero-Shot Prompting
Zero-shot means giving the model a task with no examples. You rely entirely on the model's training data to understand what you want. This works well for straightforward tasks where the expected format is obvious.
// Zero-shot: simple classification const prompt = `Classify the following support ticket as one of: bug, feature_request, question, billing Ticket: "The export button doesn't work when I have more than 100 rows" Category:`; // Output: bug
Zero-shot works when the task is unambiguous. For anything with nuance or specific formatting requirements, you'll need more structure.
Few-Shot Prompting
Few-shot provides 2-5 examples of the desired input/output pattern before your actual request. The model picks up on the pattern and replicates it. This is your most reliable technique for consistent formatting.
// Few-shot: consistent extraction const prompt = `Extract the action items from meeting notes. Meeting: "We need to update the API docs by Friday and John will handle the database migration next week." Action items: - Update API docs (deadline: Friday) - Database migration (owner: John, deadline: next week) Meeting: "Sarah is redesigning the dashboard. We also agreed to deprecate the v1 endpoint after March." Action items: - Redesign dashboard (owner: Sarah) - Deprecate v1 endpoint (deadline: after March) Meeting: "The team decided to switch to TypeScript for new services and Mike will set up the CI pipeline tomorrow." Action items:`; // Model follows the exact format from examples
The key insight: your examples teach format, tone, and level of detail simultaneously. Choose diverse examples that cover different scenarios the model will encounter.
Chain-of-Thought (CoT) Prompting
Chain-of-thought asks the model to reason step by step before reaching a conclusion. This dramatically improves accuracy on tasks involving logic, math, or multi-step analysis. The reasoning process itself helps the model avoid errors.
// Chain-of-thought: debugging analysis
const prompt = `Analyze this error and suggest a fix.
Think through the problem step by step.
Error: "TypeError: Cannot read properties of undefined (reading 'map')"
Code:
function UserList({ data }) {
return data.users.map(user => <li>{user.name}</li>)
}
Step-by-step analysis:`;
// Model reasons through:
// 1. The error says something is undefined
// 2. .map is called on data.users
// 3. Either data is undefined or data.users is undefined
// 4. The component doesn't handle loading/empty states
// 5. Fix: add null check or default valueYou can trigger CoT simply by adding "Think step by step" or "Let's work through this" to your prompt. For production use, you might want to separate the reasoning from the final answer so you can parse just the conclusion.
System Prompts
System prompts define the model's behavior for an entire conversation. They set constraints, persona, capabilities, and output rules that apply to every response. Think of them as configuration that shapes all subsequent interactions.
// Effective system prompt structure const systemPrompt = `You are a code review assistant for a TypeScript/React codebase. Rules: - Focus on bugs, security issues, and performance problems - Ignore style/formatting (handled by ESLint) - Rate severity as: critical, warning, or suggestion - Always explain WHY something is a problem, not just what - If the code looks fine, say so — don't invent issues Output format: For each issue found, respond with: [SEVERITY] file:line — description Reason: why this matters Fix: suggested code change`;
Good system prompts are specific about what to do AND what not to do. The "don't invent issues" instruction above prevents the model from hallucinating problems that don't exist — a common failure mode.
Role Prompting
Role prompting assigns the model a specific persona or expertise. This isn't just flavor text — it activates relevant knowledge and adjusts the depth and style of responses. A model told to be a "senior security engineer" will catch vulnerabilities that a generic prompt misses.
// Role prompting examples
const roles = {
security: "You are a senior application security engineer reviewing code for OWASP Top 10 vulnerabilities.",
performance: "You are a performance engineer optimizing Node.js applications for high-throughput production loads.",
mentor: "You are a patient senior developer explaining concepts to a junior teammate. Use analogies and build from fundamentals.",
architect: "You are a system architect evaluating this design for scalability, maintainability, and failure modes."
};
// Each role produces fundamentally different analysis of the same codeCombine role prompting with specific constraints for best results. "You are a security engineer" is okay. "You are a security engineer who only flags issues with clear exploit paths, not theoretical risks" is better.
Output Formatting (JSON Mode)
For production applications, you almost always need structured output you can parse programmatically. JSON mode forces the model to return valid JSON matching your schema. This eliminates the need for fragile regex parsing.
// OpenAI: JSON mode with schema
const response = await openai.chat.completions.create({
model: "gpt-4o",
response_format: { type: "json_object" },
messages: [
{ role: "system", content: "Return a JSON object with: { sentiment: 'positive'|'negative'|'neutral', confidence: 0-1, topics: string[] }" },
{ role: "user", content: "Analyze: 'The new API is fast but the docs are confusing'" }
]
});
// Guaranteed valid JSON:
// { "sentiment": "mixed", "confidence": 0.85, "topics": ["api-performance", "documentation"] }
// Anthropic: Tool use for structured output
const response = await anthropic.messages.create({
model: "claude-sonnet-4-20250514",
tools: [{
name: "analyze_sentiment",
input_schema: {
type: "object",
properties: {
sentiment: { type: "string", enum: ["positive", "negative", "neutral", "mixed"] },
confidence: { type: "number", minimum: 0, maximum: 1 },
topics: { type: "array", items: { type: "string" } }
},
required: ["sentiment", "confidence", "topics"]
}
}],
tool_choice: { type: "tool", name: "analyze_sentiment" },
messages: [{ role: "user", content: "Analyze: 'The new API is fast but the docs are confusing'" }]
});Always include an example of the expected output in your prompt, even when using JSON mode. The model uses the example to understand the level of detail and specific values you expect.
Techniques Comparison
Quick reference for choosing the right technique based on your task:
| Technique | Best For | Token Cost | Consistency | Complexity |
|---|---|---|---|---|
| Zero-shot | Simple, clear tasks | Low | Medium | Low |
| Few-shot | Format-sensitive outputs | Medium | High | Medium |
| Chain-of-thought | Reasoning, math, logic | High | High | Low |
| System prompt | Persistent behavior rules | Medium | High | Medium |
| Role prompting | Domain-specific analysis | Low | Medium | Low |
| JSON mode | Structured API responses | Medium | Very High | Medium |
Production Code Examples
Here's how these techniques look in real application code:
OpenAI: Multi-Technique Prompt
import OpenAI from 'openai';
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
async function classifyAndRoute(ticket) {
const response = await openai.chat.completions.create({
model: "gpt-4o",
temperature: 0.1, // Low temp for consistent classification
response_format: { type: "json_object" },
messages: [
{
role: "system",
content: `You are a support ticket classifier for a SaaS product.
Classify each ticket and return JSON with:
{
"category": "bug" | "feature" | "question" | "billing" | "urgent",
"priority": 1-5 (5 = most urgent),
"team": "engineering" | "product" | "support" | "billing",
"summary": "one-line summary under 100 characters"
}
Rules:
- Anything mentioning "data loss", "can't login", or "security" is priority 5
- Feature requests are always priority 2 unless from enterprise customers
- When unsure between categories, choose the one that gets faster response`
},
{
role: "user",
content: `Classify this ticket: "${ticket}"`
}
]
});
return JSON.parse(response.choices[0].message.content);
}
// Usage
const result = await classifyAndRoute(
"I can't export my data and I have a board meeting tomorrow"
);
// { category: "bug", priority: 4, team: "engineering", summary: "Export functionality broken - time-sensitive" }Anthropic: Chain-of-Thought with Structured Output
import Anthropic from '@anthropic-ai/sdk';
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
async function reviewCode(code, language) {
const response = await anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 2048,
system: `You are a code reviewer. Analyze code for bugs, security issues, and performance problems.
Think through each potential issue step by step before deciding if it's a real problem.
Only flag issues you're confident about — false positives waste developer time.
Use XML tags to structure your response:
<reasoning>Your step-by-step analysis</reasoning>
<issues>
<issue severity="critical|warning|suggestion">
<location>line or function name</location>
<description>what's wrong</description>
<fix>how to fix it</fix>
</issue>
</issues>
<summary>One paragraph overall assessment</summary>`,
messages: [
{ role: "user", content: `Review this ${language} code:\n\n${code}` }
]
});
return response.content[0].text;
}
// The model reasons through each line, then only reports genuine issues
// Separating <reasoning> from <issues> lets you show/hide the thinkingCombining Techniques: RAG + Few-Shot + JSON
async function answerFromDocs(question, relevantDocs) {
const response = await openai.chat.completions.create({
model: "gpt-4o",
response_format: { type: "json_object" },
messages: [
{
role: "system",
content: `Answer developer questions using ONLY the provided documentation.
Return JSON: { "answer": "...", "sources": ["doc_id"], "confidence": 0-1 }
If the docs don't contain the answer, set confidence to 0 and answer to
"I don't have enough information to answer this accurately."
Examples:
Q: "How do I authenticate?" + docs about auth
→ { "answer": "Use Bearer tokens in the Authorization header...", "sources": ["auth-guide"], "confidence": 0.95 }
Q: "What's the pricing?" + docs about API endpoints
→ { "answer": "I don't have enough information to answer this accurately.", "sources": [], "confidence": 0 }`
},
{
role: "user",
content: `Documentation:\n${relevantDocs.map(d => `[${d.id}] ${d.content}`).join('\n\n')}\n\nQuestion: ${question}`
}
]
});
return JSON.parse(response.choices[0].message.content);
}Common Prompt Engineering Mistakes
These mistakes account for most of the frustration developers experience with LLMs:
❌ Being too vague
"Write me a function" — the model doesn't know the language, framework, error handling style, or what the function should actually do.
✓ Fix: Include language, input/output types, edge cases, and one example of expected behavior.
❌ Prompt is too long with irrelevant context
Pasting an entire 500-line file when the question is about one function. The model gets confused by noise.
✓ Fix: Include only the relevant code plus 5-10 lines of context. Reference other files by name if needed.
❌ No examples for format-sensitive tasks
"Extract entities from this text" without showing what entities look like in your domain. The model guesses the format.
✓ Fix: Always include 2-3 examples showing the exact output format you expect.
❌ Ignoring temperature settings
Using default temperature (1.0) for classification or data extraction. Higher temperature = more creative = less consistent.
✓ Fix: Use 0.0-0.3 for deterministic tasks (classification, extraction). Use 0.7-1.0 for creative tasks (writing, brainstorming).
❌ Not handling edge cases in the prompt
Asking for JSON output but not specifying what happens when input is empty or invalid. The model invents a response.
✓ Fix: Explicitly state: 'If input is empty, return { error: "no input" }. If you're unsure, say so.'
❌ Treating prompts as write-once
Writing a prompt, testing it once, shipping it. Prompts need iteration like any other code.
✓ Fix: Test with diverse inputs, log failures, track accuracy metrics, and version your prompts in source control.
Best Practices
Start with the simplest prompt that could work, then add complexity only where needed
Use delimiters (XML tags, triple backticks, headers) to clearly separate instructions from content
Specify the output format explicitly — don't make the model guess what you want
Include negative instructions (what NOT to do) to prevent common failure modes
Test prompts with adversarial inputs — empty strings, very long text, edge cases
Version your prompts in source control alongside your application code
Log prompt/response pairs in production so you can identify failures and iterate
Use the lowest temperature that produces acceptable variety for your use case
Advanced Prompt Engineering Tips
Once you've mastered the basics, these techniques take your prompts to the next level:
Prompt Chaining
Break complex tasks into multiple sequential prompts where the output of one becomes the input of the next. This produces better results than trying to do everything in a single prompt because each step gets the model's full attention.
// Prompt chaining example: code review pipeline
const step1 = await llm("Identify the top 3 bugs in this code: ...");
const step2 = await llm(`For each bug, suggest a fix:\n${step1}`);
const step3 = await llm(`Generate the corrected code:\n${step2}`);
// Each step builds on the previous, producing higher quality than one-shotSelf-Consistency (Multiple Samples)
For critical decisions, generate multiple responses with higher temperature and pick the most common answer. This reduces the chance of a single unlucky generation leading you astray.
Prompt Templates with Variables
Treat prompts as templates in your codebase — parameterized strings that accept variables. This makes them testable, versionable, and reusable across different inputs.
// Prompt template pattern
const templates = {
codeReview: ({ language, code, focus }) => `
Review this ${language} code. Focus on: ${focus}.
Only report issues you're confident about.
\`\`\`${language}
${code}
\`\`\`
Return JSON: [{ "line": number, "severity": "critical"|"warning", "issue": string, "fix": string }]
`,
summarize: ({ text, maxWords, audience }) => `
Summarize the following for a ${audience} audience in under ${maxWords} words:
${text}
`
};Frequently Asked Questions
What is prompt engineering?
Prompt engineering is the practice of designing and optimizing inputs to AI language models to get accurate, useful, and consistent outputs. It involves techniques like few-shot examples, chain-of-thought reasoning, and structured output formatting.
Do I need prompt engineering if I use the latest models?
Yes. Even the most capable models benefit from well-structured prompts. Better prompts reduce token usage, improve accuracy, and make outputs more predictable — especially for production applications where consistency matters.
What is the difference between zero-shot and few-shot prompting?
Zero-shot gives the model a task with no examples. Few-shot includes 2-5 examples of the desired input/output format before the actual task, helping the model understand the expected pattern and level of detail.
What is chain-of-thought prompting?
Chain-of-thought asks the model to show its reasoning step by step before arriving at a final answer. This dramatically improves accuracy on complex tasks like debugging, math problems, and multi-step code generation.
How do system prompts work?
System prompts are instructions sent at the beginning of a conversation that define the model's behavior, persona, constraints, and output format. They persist across the entire conversation and shape all subsequent responses.
Can I force an LLM to return valid JSON?
Yes. OpenAI supports response_format: { type: 'json_object' }, and Anthropic supports tool use with defined schemas. Always include a JSON example in your prompt for best results.
How many few-shot examples should I include?
Typically 2-5 examples work well. More examples improve consistency but consume tokens. Start with 3 diverse examples covering edge cases and typical inputs, then adjust based on output quality.
Does prompt engineering work the same across different models?
Core techniques like few-shot and chain-of-thought work universally. However, each model has nuances — Claude responds well to XML tags, GPT-4 works well with markdown, and different models have different context limits that affect prompt design.
Related Articles
AI Context Windows Explained
Why token limits matter and how to manage them
RAG Explained for Developers
Ground your AI in real data with retrieval-augmented generation
How AI Coding Agents Work
Architecture, workflow & future of AI programming tools
AI Agents vs AI Workflows
When to use autonomous agents vs structured pipelines
Conclusion
Prompt engineering isn't a dark art — it's a set of learnable patterns that make LLMs dramatically more useful. The techniques here (few-shot, chain-of-thought, system prompts, JSON mode) cover the vast majority of production use cases.
The most important takeaway: treat prompts as code. Version them, test them, iterate on them, and measure their performance. A well-engineered prompt running on a smaller model will often outperform a lazy prompt on the most expensive model. That's the leverage prompt engineering gives you.
Start with the simplest approach that works and add complexity only when you need it. Most tasks need just a clear system prompt and a few examples. Save the advanced techniques for when you actually hit a wall.
