Prompt Engineering for Claude Code: Patterns That Actually Work
Why Prompting Claude Code Is Different
Why Prompting Claude Code Is Different
Prompt engineering for Claude Code is not the same as prompt engineering for ChatGPT or a general-purpose LLM. The difference is context. Claude Code can see your actual code, run your actual commands, and verify its actual output. This means the prompting patterns that work are more about directing an agent than crafting the perfect question.
After thousands of interactions with Claude Code across dozens of projects, I have identified the patterns that consistently produce the best results. These are not theoretical — they are battle-tested in production React and Next.js codebases.
Pattern 1: The Constraint Sandwich
The most effective prompts follow this structure: What to do, How to do it, What NOT to do.
me: Create a DatePicker component in components/ui/DatePicker/.
Requirements:
- Accept value (Date | null) and onChange ((date: Date) => void) props
- Use our existing Popover component for the dropdown
- Support min/max date constraints
- Support disabled state
- Keyboard navigable (arrow keys for days, tab for month/year)
Constraints:
- Do NOT use any external date picker library
- Do NOT use moment.js — use native Date or date-fns
- Do NOT use inline styles — Tailwind only
- Follow the same pattern as our existing Select component
The constraints are as important as the requirements. Without them, Claude Code might reach for a library you do not want, use a styling approach you do not use, or follow patterns that do not match your project.
Pattern 2: Reference Existing Code
Instead of describing patterns from scratch, point Claude Code at existing code that exemplifies what you want:
me: Create a useOrders hook following the exact same pattern as
hooks/useProducts.ts. Same query structure, same error handling,
same cache invalidation approach. The API endpoints are:
GET /api/orders (list)
GET /api/orders/:id (detail)
POST /api/orders (create)
PATCH /api/orders/:id (update)
This is more effective than describing the pattern because Claude Code reads the reference file and replicates the actual implementation, including subtle details you might forget to mention.
Pattern 3: Incremental Complexity
For complex tasks, build up incrementally rather than asking for everything at once:
me: Step 1: Create the basic NotificationBell component that shows an
icon with an unread count badge. Hard-code the count to 5 for now.
Make it visually match our existing IconButton component.
After reviewing and approving:
me: Step 2: Create a useNotifications hook that fetches notifications
from GET /api/notifications. Use TanStack Query. Return
{ notifications, unreadCount, isLoading, error }.
After reviewing:
me: Step 3: Wire up NotificationBell to use useNotifications.
Replace the hard-coded count with the real unreadCount.
Add a click handler that opens a NotificationList dropdown.
This pattern gives you checkpoints to review and course-correct. It also produces cleaner code because each step has a focused scope.
Pattern 4: The Persona Prompt
When you need Claude Code to evaluate or review rather than implement, give it a perspective:
me: Review the auth middleware in middleware/auth.ts as if you were
a security auditor. Check for:
- Token validation completeness
- Timing attack vulnerabilities
- Header injection possibilities
- Missing edge cases in the token refresh flow
Report findings with severity levels.
Or for architecture:
me: Look at our data fetching approach across the app (services/,
hooks/, and the components that use them). As a frontend architect
doing an audit, identify:
- Inconsistencies in patterns
- Opportunities for shared abstractions
- Potential performance issues (over-fetching, waterfall requests)
- Missing error handling
Pattern 5: The Verification Loop
Always include verification in your prompts:
me: Refactor the UserProfile component to extract the avatar upload
logic into a useAvatarUpload hook.
After the refactoring:
1. Run TypeScript type check (npm run type-check)
2. Run the tests for UserProfile (npm test -- UserProfile)
3. If anything fails, fix it
4. Show me the final diff
The "if anything fails, fix it" part is critical. It tells Claude Code to iterate rather than hand you broken code and wait for your next instruction.
Pattern 6: Scope Boundaries
Explicitly define what is and is not in scope:
me: Update the pricing page to show annual pricing toggle.
IN SCOPE:
- PricingPage component
- PricingCard component
- The price display logic
- New toggle component
OUT OF SCOPE:
- Backend pricing logic (prices come from API, do not change API calls)
- Payment flow (that is a separate feature)
- Existing PricingCard tests (I will update them separately)
This prevents Claude Code from making well-intentioned but unwanted changes to related code.
Pattern 7: Error Context
When asking Claude Code to fix an issue, provide the full error context:
me: The build is failing with this TypeScript error:
Type 'string | undefined' is not assignable to type 'string'.
Type 'undefined' is not assignable to type 'string'.
at components/Dashboard/MetricsCard.tsx:24:5
The issue started after we updated the API response types
in types/api/metrics.ts. The API now returns some fields as
optional that were previously required.
Fix the component to handle the optional fields properly.
Use sensible defaults where appropriate. Do NOT use non-null
assertions (!) — handle the undefined case explicitly.
The historical context ("started after we updated") helps Claude Code understand the root cause, not just the symptom.
Pattern 8: Output Format Specification
When you need specific output, say so:
me: Analyze all API calls in services/ and create a summary.
Format the output as a markdown table with columns:
- Service file
- Function name
- HTTP method
- Endpoint
- Auth required (yes/no)
- Has error handling (yes/no)
- Has loading state (yes/no)
This is especially useful for audits, documentation generation, and codebase analysis.
Anti-Patterns: What Does Not Work
The Vague Ask
# Bad
me: Make the dashboard better
# Good
me: Improve the Dashboard page performance by:
1. Memoizing the chart data computation in useMetrics
2. Adding virtualization to the activity feed list
3. Lazy loading the analytics section below the fold
The Mega-Prompt
# Bad
me: Build the entire checkout flow with cart management, address form,
payment integration, order confirmation, email notification, and
error handling for all edge cases.
# Good
me: Let's build the checkout flow. Start with the cart summary component
that displays items, quantities, and total. Use the CartItem type
from types/cart.ts and the useCart hook from hooks/useCart.ts.
The Assumption Prompt
# Bad
me: Fix the bug
# Good
me: Users report that the filter dropdown on the Products page resets
when they switch tabs. The expected behavior is that filters persist
across tab switches. The filter state is managed in useProductFilters.
Check if the hook is reinitializing when the tab changes.
The No-Context Prompt
# Bad
me: Add validation
# Good
me: Add Zod validation to the contact form in components/ContactForm.tsx.
Validate: email (valid format), name (1-100 chars), message (1-1000
chars, no HTML). Display errors below each field using our FormError
component. Prevent submission while validation fails.
The Meta-Pattern: Think Like an Architect
The underlying principle across all these patterns is the same: think like an architect giving instructions to a capable but new team member. Be specific about the what. Be clear about the constraints. Reference existing patterns. Include verification steps. Define scope.
The developers who complain that AI tools produce mediocre code are almost always giving mediocre prompts. Claude Code is exactly as good as your ability to articulate what you want. That is not a limitation — it is a feature. It forces you to think clearly about your code, which makes you a better developer regardless.