Prompting for Code Generation
The Goal: First-Try Production Code
The difference between a developer who uses AI effectively and one who does not often comes down to code generation prompts. An effective prompt produces code that compiles, handles edge cases, follows your project conventions, and is ready for review. An ineffective prompt produces code that technically works but requires 20 minutes of manual cleanup before it is mergeable.
The techniques in this lesson will get you to first-try production code for most tasks. Not always -- complex logic still requires iteration. But for the 80% of code generation tasks that follow known patterns, you can and should expect correct output on the first attempt.
Specifying the Tech Stack Explicitly
Never assume the AI knows your stack. Even if it can read your package.json, explicitly stating the stack narrows the solution space and prevents the AI from generating code with incompatible libraries or patterns.
Bad:
Create a user registration endpoint
Good:
Create a user registration endpoint in our Express.js API using TypeScript.
Stack: Express 4, TypeScript 5, Prisma ORM with PostgreSQL, Zod for input
validation, bcrypt for password hashing. The endpoint should be in
src/routes/auth.ts.
The stack declaration eliminates ambiguity. The AI will not use Mongoose (MongoDB), Sequelize, or raw SQL. It will not generate JavaScript instead of TypeScript. It will use Zod instead of Joi or Yup for validation.
Including File Context
One of the most powerful code generation techniques is referencing existing files. This gives the AI a concrete example to match.
In src/components/Button.tsx, create a new React component called IconButton.
It should follow the exact same pattern as the existing Button component in
the same file: same prop interface style, same Tailwind class approach, same
forwardRef pattern. The difference is that IconButton takes an additional
"icon" prop (a React.ReactNode) and renders it before the children.
The "existing pattern" technique works because the AI can read that file and directly match the structure. Compare this to describing the pattern in words -- saying "use forwardRef with proper TypeScript generics and Tailwind utility classes with CVA variants" is both more verbose and less precise than saying "follow the pattern in Button.tsx."
The Reference File Pattern
When generating code that must integrate with an existing codebase, provide a reference:
Create a new API route handler for GET /api/orders/:id. Use
src/routes/products.ts as the reference for:
- Error handling pattern (try/catch with AppError)
- Response format (res.json with status and data fields)
- Authentication middleware usage
- Prisma query patterns
The order should include its line items and the customer information.
Include proper 404 handling when the order is not found.
Defining Behavior: Inputs, Outputs, Edge Cases
Specify what goes in, what comes out, and what happens when things go wrong.
Vague:
Write a function to process payments
Specific:
Write a function processPayment in src/services/payment.ts:
Input: { amount: number, currency: string, customerId: string, paymentMethodId: string }
Output: { success: boolean, transactionId: string | null, error: string | null }
Behavior:
- Validate amount is positive and currency is one of ['USD', 'EUR', 'GBP']
- Call the Stripe API using our existing stripe client from src/lib/stripe.ts
- On success, return the transaction ID
- On Stripe card_declined error, return { success: false, error: 'Card declined' }
- On Stripe rate_limit error, retry once after 1 second
- On any other error, log the error with our logger and return a generic error message
- Never expose Stripe error details to the caller
This prompt leaves nothing to interpretation. The AI knows the function signature, the return type, the happy path, and every error scenario. The output will be precise.
Code Generation Anti-Patterns
Certain phrases in prompts consistently produce poor results because they are subjective or unmeasurable:
"Write clean code" -- Clean according to whom? Uncle Bob? Your team lead? The Go community? Instead, specify what you mean: "Use early returns instead of nested ifs," "Keep functions under 20 lines," "Use descriptive variable names like customerEmail instead of ce."
"Follow best practices" -- Whose best practices? The term means different things in different communities. Instead: "Follow the error handling pattern from our existing codebase," or "Use parameterized queries to prevent SQL injection."
"Optimize this" -- Optimize for what? Speed? Memory? Readability? Bundle size? Instead: "Reduce the number of database queries from N+1 to a single query with joins," or "Memoize this computation so it only runs when the input changes."
"Make it production-ready" -- This phrase means everything and nothing. Instead, list the specific qualities: "Add error handling, input validation, logging, and TypeScript types."
Real Examples
Generating an API Endpoint
Create a REST endpoint in src/routes/users.ts for PATCH /api/users/:id.
Purpose: Allow users to update their profile (name, bio, avatarUrl).
Requirements:
- Use the existing authMiddleware to verify the JWT token
- Users can only update their own profile (compare req.user.id with params.id)
- Validate input with Zod: name (string, 1-100 chars, optional), bio (string,
max 500 chars, optional), avatarUrl (valid URL, optional)
- Use Prisma to update only the provided fields
- Return the updated user object (exclude passwordHash from response)
- Return 403 if user tries to update another user's profile
- Return 404 if user ID does not exist
Follow the same pattern as the GET /api/users/:id handler in the same file.
Generating a React Component
Create a React component in src/components/UserProfile.tsx.
Props:
- user: { id: string, name: string, email: string, avatarUrl?: string, bio?: string }
- isEditable: boolean
- onSave: (updates: Partial<User>) => Promise<void>
Behavior:
- Display mode: Shows user info in a card layout using our existing Card component
- Edit mode (when isEditable and user clicks "Edit"): Inline form with
controlled inputs for name, bio, and avatarUrl
- Save button calls onSave with only the changed fields
- Show loading state during save (disable form, show spinner)
- Show error toast on save failure using our existing useToast hook
- Revert to display mode on successful save
Styling: Use Tailwind CSS. Match the styling of our existing ProductCard
component in src/components/ProductCard.tsx.
Generating a Utility Function
Create a utility function in src/utils/retry.ts:
export async function withRetry<T>(
fn: () => Promise<T>,
options?: { maxAttempts?: number, delayMs?: number, backoff?: boolean }
): Promise<T>
Default options: maxAttempts = 3, delayMs = 1000, backoff = true
Behavior:
- Call fn(). If it succeeds, return the result.
- If it throws, wait delayMs and retry.
- If backoff is true, double the delay on each retry (1s, 2s, 4s).
- After maxAttempts failures, throw the last error.
- Log each retry attempt with console.warn including attempt number and error
message.
Include JSDoc documentation and export the options type.
The Generate-Then-Refine Workflow
For complex code generation, use a two-step workflow:
Step 1 -- Generate the first draft:
Create the OrderService class in src/services/OrderService.ts with methods:
createOrder, getOrderById, getOrdersByCustomer, cancelOrder. Use Prisma for
database access. Include basic error handling.
Step 2 -- Refine with specific feedback:
Good structure. Now refine the OrderService:
1. Add input validation using Zod to createOrder
2. The cancelOrder method should check if the order status allows cancellation
(only 'pending' and 'confirmed' orders can be cancelled)
3. Add pagination to getOrdersByCustomer (cursor-based, 20 per page)
4. Wrap all Prisma calls in try/catch that converts PrismaClientKnownRequestError
to our custom AppError class
This workflow is faster than trying to specify everything upfront for complex tasks. The first pass gives you the skeleton, the second pass adds the specifics. Each step is simple enough for the AI to execute accurately.
Over-Specification vs Under-Specification
There is a balance between giving the AI too much detail and too little. Under-specification produces generic code. Over-specification wastes your time writing the prompt and can actually confuse the AI if your instructions conflict.
The rule: specify the things that make your code unique. Standard patterns (a basic CRUD endpoint, a simple React component) need less specification because the AI has seen thousands of examples. Unusual requirements (custom business logic, nonstandard error handling, integration with proprietary APIs) need more specification because the AI cannot guess them.
When in doubt, start with a moderately detailed prompt. If the output misses something, add that specific detail in a follow-up rather than rewriting the entire prompt.