TechLead
Lesson 5 of 25
8 min read
AI-Native Engineering

Prompt Engineering for Code Generation

Learn the specific techniques for writing prompts that produce high-quality, production-ready code — from vague instructions to precise, effective directives

How Code Prompting Differs from General Prompting

Prompting an AI to write code is fundamentally different from prompting it for text. When you ask for an essay, "pretty good" is often acceptable. When you ask for code, it either works or it does not. It either follows your patterns or it introduces inconsistency. It either handles edge cases or it crashes in production.

The art of code prompting is finding the sweet spot between being too vague (getting generic, boilerplate code) and being too prescriptive (fighting the AI's ability to write good code). This lesson teaches you where that sweet spot is.

The Prompting Spectrum

  • Too Vague: "Add authentication" — Claude guesses everything, likely wrong choices
  • Sweet Spot: "Add NextAuth.js with GitHub provider. Follow the existing auth patterns. Add a sign-in button to the Header." — Claude knows what, has freedom on how
  • Too Prescriptive: Line-by-line instructions for every file — You might as well write it yourself, and you are preventing Claude from applying its knowledge

The Five Principles of Effective Code Prompts

1. Describe WHAT, Not HOW

Tell Claude the desired outcome and behavior. Let it determine the implementation. Claude has been trained on millions of code patterns — it often knows better implementations than you would specify.

# BAD: Specifying implementation details
> Create a function that takes an array of objects, creates a new Map,
  loops through the array with forEach, and uses the id field as key
  and the object as value. Return the Map.

# GOOD: Specifying behavior
> Create a utility function that converts an array of objects to a
  lookup map keyed by their ID field. It should be type-safe and
  handle duplicate IDs by keeping the last occurrence.

2. Provide Context on WHY

When Claude understands the purpose, it makes better design decisions. "Build a rate limiter" produces different code than "Build a rate limiter for our public API that handles 10K requests per second and needs to work across multiple server instances."

# WITHOUT context (Claude guesses the use case)
> Add caching to the getUser function.

# WITH context (Claude makes informed decisions)
> Add caching to the getUser function. This is called on every page
  load and the user data rarely changes (maybe once per session).
  We need to reduce database load since we are hitting connection
  limits during peak hours. Use Redis since we already have it
  configured in app/lib/redis.ts.

3. Reference Existing Patterns

The most powerful technique in codebase-aware tools like Claude Code: point to an existing example and say "do it like that."

# GENERIC output
> Create an API route for managing blog posts.

# CONSISTENT output
> Create an API route for managing blog posts. Follow the exact same
  pattern used in app/api/products/route.ts — same error handling,
  same validation approach, same response format.

4. Specify Constraints Explicitly

Tell Claude what to avoid. Constraints are just as important as requirements.

# Constraints that prevent common issues
> Add image upload to the profile page. Constraints:
  - No new dependencies — use the existing S3 client in app/lib/s3.ts
  - Max file size: 5MB, validated client-side and server-side
  - Only accept JPEG, PNG, and WebP formats
  - Do not break the existing form submission flow
  - Must work with our static export (no server-side processing)

5. Define Done Clearly

Tell Claude what success looks like. What should work when the task is complete?

# Vague completion criteria
> Add form validation to the contact form.

# Clear completion criteria
> Add form validation to the contact form. When done:
  - All fields should show inline error messages on blur
  - The submit button should be disabled until all required fields are valid
  - Email field should validate format
  - Phone field should accept international formats
  - The form should not submit if validation fails
  - Add tests that verify all validation rules

10 Before/After Prompt Examples

Bad Prompt Good Prompt
"Add a modal component" "Add a reusable Modal component following the pattern in Dialog.tsx. It should trap focus, close on Escape, and support a title, body, and action buttons."
"Fix the performance" "The user list page at app/users/page.tsx re-renders every component when any filter changes. Memoize the UserCard components and optimize the filter state so only affected cards re-render."
"Write tests" "Write Vitest tests for app/lib/pricing.ts covering: standard pricing, volume discounts, coupon codes, tax calculations, and edge cases like zero quantity and negative prices."
"Make it responsive" "Make the dashboard page responsive. On mobile (<768px): stack the sidebar below the main content, collapse the chart grid to single column, and hide the secondary metrics panel behind a 'Show more' toggle."
"Add error handling" "Add error handling to all API calls in app/lib/api.ts. Use the AppError class from app/lib/errors.ts. Retry on 5xx errors (max 3 attempts with exponential backoff). Show user-friendly messages via the toast system for 4xx errors. Log all errors to our logging service."
"Refactor this file" "app/lib/utils.ts is 800 lines with mixed concerns. Split it into: dateUtils.ts, formatUtils.ts, validationUtils.ts, and arrayUtils.ts. Update all imports across the codebase."
"Add dark mode" "Add dark mode using next-themes. Add a toggle to the Header. All Tailwind classes should use dark: variants. Test with the existing color palette in tailwind.config.ts. Do not change any light mode styles."
"Optimize the database queries" "The /api/dashboard endpoint makes 12 sequential database queries. Identify which queries can run in parallel using Promise.all. Add an index for the user_id lookups if one does not exist. Target: under 200ms total response time."
"Add TypeScript types" "Convert app/lib/analytics.js to TypeScript. Define proper types for all function parameters and return values. Use strict mode — no 'any' types. The analytics events should use a discriminated union type."
"Add logging" "Add structured logging to the payment service using our existing logger in app/lib/logger.ts. Log: payment initiated, payment succeeded, payment failed (with error details), and refund processed. Include correlation IDs for request tracing. Never log card numbers or CVVs."

Anti-Patterns to Avoid

Prompt Anti-Patterns

  • The Micromanager: Specifying every variable name, loop structure, and line of code. If you are this specific, you are writing the code yourself with extra steps. Trust the AI to handle implementation details.
  • The Wishful Thinker: "Build me a production-ready payment system" with no constraints or context. Complex systems need iterative development, not single-prompt miracles.
  • The Copy-Paster: Dumping an entire error log without highlighting the relevant parts. Give Claude the signal, not the noise.
  • The Re-Prompter: Rejecting AI output and re-prompting from scratch instead of saying "good, but change X and Y." Build on what Claude gave you.
  • The Context Withholder: Asking for changes without mentioning the constraint that makes the task hard. "Add caching" when the real challenge is "add caching that works with our multi-region deployment."

Advanced Techniques

Chain of Prompts for Complex Features

# Instead of one massive prompt, break into a chain:

# Step 1: Plan
> /plan I need to add a role-based access control (RBAC) system.
  We have three roles: admin, editor, viewer. Plan the implementation
  including database schema, middleware, and UI changes.

# Step 2: Schema first
> Implement the database schema and migration for roles and permissions.

# Step 3: Middleware
> Now add the auth middleware that checks permissions. Use the schema
  you just created.

# Step 4: UI
> Update the UI to show/hide elements based on user role. Add a
  role management page for admins.

# Step 5: Tests
> Write tests for the RBAC system. Cover all three roles and
  edge cases like expired sessions and role changes mid-session.

The "Show Me an Example" Technique

# When you are not sure what approach to take:
> Show me 3 different approaches to implementing real-time notifications
  in this Next.js app. For each approach, show a brief code example,
  list the pros and cons, and recommend which one fits our architecture
  best. Do not implement anything yet — I want to choose first.

The Golden Rule

The best prompt is one that a senior engineer on your team could follow without asking questions. If your prompt would make a capable human say "but wait, what about..." then Claude will have the same confusion. Be clear on the WHAT and the WHY. Let Claude handle the HOW. Specify constraints that are not obvious. Reference existing patterns when consistency matters.

Summary

Effective code prompting is a learnable skill that dramatically impacts your productivity with AI tools. Focus on describing outcomes, providing business context, referencing existing patterns, stating constraints, and defining success criteria. Avoid micromanaging implementation details or writing prompts so vague that Claude has to guess at everything. With practice, you will develop an intuition for exactly how much context Claude needs for any given task.

Continue Learning