Prompting Claude through an API requires a different mindset than prompting through chat.

In chat, you can clarify, iterate, and course-correct. In an API, you get one shot. Your prompt needs to produce consistent, parseable output every time—because your code is processing the response automatically.

Here are the patterns that work in production.


Pattern 1: Structured Output with JSON

Problem: Free-form text responses are hard to parse programmatically.

Solution: Explicitly request JSON and provide the schema.

const prompt = `Analyze this customer review and extract sentiment.

Review: "${reviewText}"

Respond ONLY with JSON in this exact format:
{
  "sentiment": "positive" | "negative" | "neutral",
  "confidence": 0.0 to 1.0,
  "keywords": ["array", "of", "strings"],
  "summary": "one sentence"
}`

const response = await callClaude(prompt)
const analysis = JSON.parse(response)

Why it works:

  • Claude understands JSON structure
  • The schema acts as a contract
  • Easier to validate and handle errors
  • No ambiguous output

Pro tip: Add Respond ONLY with valid JSON. No markdown, no explanation. to eliminate code fences.


Pattern 2: Few-Shot Examples

Problem: Claude interprets your intent differently than you expect.

Solution: Show examples of exactly what you want.

const prompt = `Convert user queries to SQL WHERE clauses.

Examples:
Input: "users who signed up last week"
Output: created_at >= NOW() - INTERVAL 7 DAY

Input: "premium users in California"
Output: plan = 'premium' AND state = 'CA'

Input: "users who haven't logged in for 30 days"
Output: last_login_at < NOW() - INTERVAL 30 DAY

Now convert this query:
Input: "${userQuery}"
Output:`

Why it works:

  • Examples disambiguate intent
  • Shows format, style, and edge cases
  • Claude mimics the pattern

When to use:

  • Classification tasks
  • Format conversion
  • Domain-specific transformations

Pattern 3: Role + Constraints

Problem: Claude is too verbose or goes off-task.

Solution: Set clear role and constraints upfront.

const systemPrompt = `You are a technical documentation generator.

Rules:
1. Output only valid markdown
2. Use code blocks with language tags
3. Maximum 200 words per section
4. No pleasantries or meta-commentary`

const message = await anthropic.messages.create({
  model: 'claude-opus-4-20250514',
  max_tokens: 1024,
  system: systemPrompt,
  messages: [{ role: 'user', content: userPrompt }],
})

Why it works:

  • System prompts persist across conversations
  • Constraints prevent scope creep
  • Role sets context for tone and style

Pattern 4: Chain of Thought for Complex Tasks

Problem: Claude makes logical errors on multi-step problems.

Solution: Ask for step-by-step reasoning.

const prompt = `Determine if this expense is reimbursable.

Company policy:
- Travel: reimbursable if business-related
- Meals: reimbursable up to $50/day while traveling
- Equipment: requires pre-approval

Expense: "${expenseDescription}"
Amount: $${amount}

Think through this step by step:
1. What category is this expense?
2. Does it meet the policy requirements?
3. Are there any red flags?

Then respond with JSON:
{
  "reimbursable": true/false,
  "reason": "explanation",
  "requiresReview": true/false
}`

Why it works:

  • Explicit steps reduce errors
  • Reasoning is visible for debugging
  • Higher accuracy on edge cases

Pattern 5: Validation and Self-Correction

Problem: Claude's first response isn't always correct.

Solution: Build validation into the prompt.

const prompt = `Generate a SQL query for: "${userRequest}"

After writing the query:
1. Check for SQL injection vulnerabilities
2. Verify all referenced columns exist in schema
3. Confirm the query answers the original question

If you find issues, revise and provide the corrected query.

Schema:
${schemaDefinition}

Respond with JSON:
{
  "query": "SELECT ...",
  "explanation": "what this query does",
  "warnings": ["any concerns or limitations"]
}`

Why it works:

  • Self-correction improves accuracy
  • Warnings surface edge cases
  • Reduces back-and-forth debugging

Pattern 6: Fallback Handling

Problem: Sometimes Claude can't complete the task.

Solution: Explicitly handle the "I don't know" case.

const prompt = `Extract the company name from this job posting.

Job posting: "${jobText}"

If you can't find a company name, respond with:
{ "companyName": null, "reason": "explanation" }

Otherwise respond with:
{ "companyName": "Company Name", "confidence": 0.0 to 1.0 }`

Why it works:

  • Prevents hallucinations
  • Structured error handling
  • Code can gracefully handle missing data

Pattern 7: Context Windows and Chunking

Problem: Input is too large for the context window.

Solution: Process in chunks with aggregation.

async function summarizeLongDocument(text: string): Promise<string> {
  const chunkSize = 4000 // ~1000 tokens
  const chunks = splitIntoChunks(text, chunkSize)

  // Summarize each chunk
  const chunkSummaries = await Promise.all(
    chunks.map(chunk =>
      callClaude(`Summarize this section in 2-3 sentences:\n\n${chunk}`)
    )
  )

  // Combine summaries
  const combined = chunkSummaries.join('\n\n')

  // Final summary
  return callClaude(
    `Synthesize these summaries into one cohesive summary:\n\n${combined}`
  )
}

Why it works:

  • Breaks large tasks into manageable pieces
  • Each step fits within token limits
  • Final synthesis maintains coherence

Pattern 8: Dynamic Prompt Construction

Problem: Different users need different prompts.

Solution: Build prompts programmatically based on context.

function buildPrompt(user: User, task: string): string {
  const parts = [`Task: ${task}`]

  // Add user context if relevant
  if (user.expertise === 'beginner') {
    parts.push('Explain in simple terms without jargon.')
  }

  // Add company context
  if (user.company) {
    parts.push(`Company context: ${user.company.industry}`)
  }

  // Add constraints
  parts.push('Respond in under 100 words.')

  return parts.join('\n\n')
}

Why it works:

  • Personalized without manual prompt writing
  • Consistent structure
  • Easy to A/B test prompt variations

Pattern 9: Streaming for Progressive Disclosure

Problem: Long responses feel slow.

Solution: Stream output and display incrementally.

async function streamResponse(prompt: string) {
  const stream = await anthropic.messages.stream({
    model: 'claude-opus-4-20250514',
    max_tokens: 2048,
    messages: [{ role: 'user', content: prompt }],
  })

  let fullResponse = ''

  for await (const chunk of stream) {
    if (chunk.type === 'content_block_delta') {
      const text = chunk.delta.text
      fullResponse += text
      // Send to frontend immediately
      sendToClient(text)
    }
  }

  return fullResponse
}

Why it works:

  • Users see results immediately
  • Feels faster even if total time is the same
  • Can stop generation if needed

Anti-Patterns to Avoid

❌ Vague Instructions

// Bad
const prompt = "Analyze this data"

// Good
const prompt = "Count unique users by signup month. Return JSON with month and count."

❌ No Output Format Specified

// Bad
const prompt = "What are the key points?"

// Good
const prompt = "Extract 3-5 key points as a JSON array of strings."

❌ Assuming Context

// Bad (assumes Claude remembers previous context)
const prompt = "Now do the same for category B"

// Good (provides all context)
const prompt = `Analyze category B using the same criteria:
- Metric 1
- Metric 2
- Metric 3`

Testing Your Prompts

Before deploying:

  1. Test edge cases: Empty input, very long input, malformed input
  2. Test consistency: Same input should produce similar output
  3. Test parsing: Ensure your code can parse every response
  4. Test failure modes: What happens when Claude can't complete the task?
// Example test suite
describe('Claude API Prompts', () => {
  it('handles empty input gracefully', async () => {
    const result = await extractKeywords('')
    expect(result).toEqual({ keywords: [], error: 'No input provided' })
  })

  it('returns valid JSON', async () => {
    const result = await extractKeywords('sample text')
    expect(() => JSON.parse(result)).not.toThrow()
  })

  it('respects max token limits', async () => {
    const result = await generateSummary(longText)
    expect(result.length).toBeLessThan(500)
  })
})

The Reality Check

No prompt is perfect. Even with these patterns, you'll encounter:

  • Edge cases you didn't anticipate
  • Responses that don't parse
  • Occasional hallucinations
  • Rate limits and timeouts

Build your application to handle imperfection. Log failures. Monitor patterns. Iterate on prompts based on real usage.

The best prompt is the one that works 95% of the time and fails gracefully the other 5%.



Official Resources


What prompt patterns have you found effective? I'm always looking to learn from others building with the Claude API.