AI Coding Agent Prompt Engineering: Real Patterns That Work
Most developers who use AI coding agents underestimate how much the quality of their prompts shapes the quality of the output. The model isn't magic. It works with what you give it, and if you give it a vague one-liner, it'll make assumptions. Sometimes those assumptions are fine. Often they're not.
This is a practical guide to prompt engineering specifically for coding workflows. Not theory. Real patterns that change how Claude Code, Cursor, and similar tools actually perform on your codebase.
The two layers most developers ignore
There's a difference between a prompt you type in the chat and the context that surrounds that prompt. Most developers only think about the first one.
The second layer is everything the agent knows before you type anything: your system prompt, your CLAUDE.md, your .cursorrules file, any files you've pinned to context. This ambient context is where the real use is. A well-structured CLAUDE.md file will make every individual prompt you type more accurate, because the model already knows your conventions, your stack, your constraints.
Think of it this way: a new developer on your team needs context before they can be useful. You'd tell them about your tech stack, your conventions, what "done" means on your team. The AI agent needs the same briefing, and CLAUDE.md (or its equivalent) is where you write it down.
Structuring a CLAUDE.md that actually works
CLAUDE.md is Claude Code's project context file. It lives in your repository root and gets injected into every conversation. Here's what to put in it.
Stack declaration (first, always). Claude Code reads this before anything else. Be specific:
## Stack
- Node.js 22, TypeScript 5.4, strict mode
- PostgreSQL 16 via Drizzle ORM (no raw SQL)
- tRPC for API layer
- React 19, Tailwind CSS 4
- Vitest for unit tests, Playwright for e2e
Conventions section. This is where you save yourself from constant corrections:
## Conventions
- Use `const` everywhere, never `let` unless reassignment is needed
- Error handling: Result pattern (never throw from service layer)
- File naming: kebab-case for files, PascalCase for components
- All new functions need JSDoc with @param and @returns
- No default exports except for Next.js pages
What not to do. Explicitly banning things is more reliable than hoping the model guesses right:
## Do not
- Do not install new dependencies without asking first
- Do not modify any file in /src/generated/ (auto-generated)
- Do not use class components in React
- Do not write TODO comments, either implement it or create a ticket
Architecture pointers. For larger codebases, tell the agent where things live:
## Architecture
- Business logic lives in /src/services/, not in route handlers
- Database queries only in /src/db/queries/
- Shared types in /src/types/shared.ts
- Auth utilities in /src/lib/auth.ts
That's it. Keep CLAUDE.md under 400 lines or it becomes too long for the model to absorb properly. The goal is precision, not coverage.
Context priming before complex tasks
Even with a good CLAUDE.md, some tasks benefit from explicit context priming at the start of a session.
The "here's what we're doing" opener. Before you ask the agent to build something non-trivial, describe the goal and the constraints in one message:
We're adding a webhook receiver for Stripe events. The handler
goes in /src/api/webhooks/stripe.ts. We use raw body parsing
(already configured), verify the signature with stripe-node,
then dispatch to a queue via /src/lib/queue.ts. The queue
function signature is already there. We don't process events
synchronously. Just queue them and return 200 immediately.
This front-loads everything the model needs. The subsequent prompts ("now add the checkout.session.completed handler") are small, precise, and usually correct on the first try.
Pinning files to context. In Claude Code, you can reference files directly:
Look at /src/services/user.service.ts before we start. We're
adding a similar service for organizations following the same pattern.
In Cursor, you use @file references in your prompt. Either way, explicitly pointing the model at the relevant existing code is more reliable than hoping it knows where to look.
The "explain first" pattern. For anything you're not sure about, ask for a plan before implementation:
Before writing any code, explain how you'd approach adding
multi-tenancy to the existing auth system. I want to understand
your plan and the tradeoffs.
This surfaces assumptions early. You'll often find the model is about to do something reasonable but wrong for your specific situation, and you can redirect before it writes 200 lines you need to throw away.
Prompt patterns for common coding tasks
Writing new features
Bad: "Add user authentication"
Better:
Add email/password authentication to the app. Use bcrypt
(already installed, v5.1.1) for password hashing. Store
sessions in the existing Redis instance via connect-redis.
Session TTL should be 7 days. The User model is in
/src/db/schema.ts and already has email and passwordHash fields.
Just implement the login and logout endpoints for now, not signup.
The specific version number, the "already installed" note, the existing schema pointer, and the scope limit ("not signup") prevent four common failure modes in one prompt.
Refactoring existing code
Bad: "Refactor this function"
Better:
Refactor calculateShippingCost() in /src/services/shipping.ts.
Right now it has too many nested conditions. Extract the weight
threshold logic into a separate function. Keep the same public
API, all tests should still pass. Don't change the business logic.
"Keep the same public API" and "don't change the business logic" are the most useful constraint phrases in refactoring prompts. They tell the model the scope of acceptable change.
Debugging
Bad: "This function isn't working"
Better:
getUserOrders() in /src/services/orders.ts returns an empty
array for users who definitely have orders. I've confirmed the
user ID is correct (tried userId 42, which has 3 orders in the
database). The SQL runs correctly when I test it directly in
psql. The issue is somewhere between the query and the return.
Walk me through what you see.
Give the agent the evidence you already have. Don't make it reconstruct your debugging steps.
Writing tests
Write Vitest unit tests for the validateAddress() function in
/src/lib/validation.ts. Cover: valid US address, missing zip
code, invalid state code, and empty input. Use the existing
test helpers in /src/test/helpers.ts. The function throws a
ValidationError (defined in /src/lib/errors.ts) on invalid input.
Pointing at existing test helpers and the error class means the model generates tests that actually match your patterns.
Cursor-specific prompt patterns
Cursor works a bit differently from Claude Code. You're typically prompting at the file level or in an inline editor mode, so context is tighter.
Using @references effectively. Cursor's @file, @code, and @docs references are powerful when used intentionally:
@file:src/api/users.ts @file:src/types/user.ts
Add a PATCH /users/:id endpoint following the same pattern
as the existing GET endpoint. Validate with zod before updating.
Composer vs inline edit. Composer (Cmd+I) is for multi-file changes. Use it for features. The inline edit (Cmd+K) is for single-function changes. The right tool saves you from getting a sprawling multi-file diff when you wanted a 10-line change.
Rejecting partial changes. Cursor will sometimes make changes you accept and then find break something. Get in the habit of typing "revert that last change and try a different approach" rather than undoing manually. The agent remembers the context and can course-correct.
Iterative prompt patterns
The most common mistake is trying to do too much in one prompt. Break work into iterations.
Good iteration structure:
- "Implement the data model for X" (schema only)
- "Add the service layer for X" (business logic, no routes yet)
- "Add the API endpoints for X" (wire up the service)
- "Write tests for the service layer"
- "Write integration tests for the endpoints"
Each step builds on a confirmed working foundation. You spend less time untangling interdependent bugs.
The checkpoint pattern. After the model implements something significant, ask:
Before we continue, does this implementation have any edge
cases or failure modes I should know about?
Claude Code will often surface issues it knew about but didn't mention (because you didn't ask). This is a fast way to catch problems before they're buried in more code.
What doesn't work
A few patterns that consistently produce poor results:
Vague quality requests. "Make this cleaner" or "make this more production-ready" are almost useless. The model's idea of clean may not match yours. Say specifically what you want: "remove the nested ternary," "add error boundaries," "replace the setTimeout with proper async/await."
Long context dumps without a clear question. Pasting 500 lines and asking "what's wrong here" wastes tokens and produces generic advice. Narrow the scope first.
Asking for multiple unrelated things in one prompt. "Add input validation, fix the error handling, and update the types" usually produces a mess. Separate these.
Over-specifying the implementation. If you dictate exactly how to implement something step by step, you lose the benefit of having a smart assistant. Give the what and the constraints; let the model figure out the how.
Keeping prompts maintainable
If you find yourself typing the same context repeatedly in different sessions, that context belongs in CLAUDE.md. If a particular prompt pattern reliably produces good output for a task type in your project, write it down. Some teams maintain a /docs/ai-prompts.md file with their most effective prompt templates. It sounds fussy but it pays off when you're onboarding someone new.
The underlying principle is simple: the more precisely you define the problem and the constraints, the less the model has to guess. Less guessing means fewer correction cycles. Fewer correction cycles means faster shipping.
For the technical side of setting up Claude Code on a new project, the official Claude Code documentation has the installation and configuration reference. For Cursor-specific configuration including .cursorrules setup, see the Cursor rules guide.