Writing Prompts for Code Generation
AI code generation is powerful but unreliable if you treat it like a search engine. The quality of the code you get back is almost entirely determined by the quality of the context you provide. This guide covers the patterns and structures that consistently produce correct, maintainable, production-appropriate code.
The Core Problem: Context Collapse
When you write "write a login function in Python," the model has to guess:
- Which framework (Flask? FastAPI? Django? plain script?)
- Which authentication method (session? JWT? OAuth?)
- What the database looks like
- What error handling style is expected
- Whether tests are needed
- What the calling code looks like
Each guess the model makes reduces the chance it produces something you can use directly. The fix is to eliminate guesses by providing context explicitly.
The Code Prompt Template
For any non-trivial code generation task, structure your prompt with these elements:
[Language and version]
[Framework and key dependencies]
[Task description]
[Constraints and requirements]
[Existing code context / signatures to match]
[Expected behavior / test cases]
[Output format]
Full Example
Language: Rust (2021 edition)
Framework: Axum 0.8, SQLx 0.8 with PostgreSQL, tokio runtime
Task: Write a handler function for POST /api/prompts that creates a new prompt.
Requirements:
- Extract the authenticated user from AuthUser extractor (already implemented)
- Accept JSON body: {title: string, body: string, tags: string[]}
- Validate: title 1-200 chars, body 1-10000 chars, max 10 tags each under 50 chars
- Generate a ULID for the ID
- Insert into the prompts table (schema below)
- Return 201 Created with the created prompt as JSON
- Return 400 for validation errors with field-level error messages
- Return 409 if slug (derived from title) already exists
Table schema:
CREATE TABLE prompts (
id CHAR(26) PRIMARY KEY,
user_id CHAR(26) NOT NULL REFERENCES users(id),
title VARCHAR(200) NOT NULL,
slug VARCHAR(220) NOT NULL UNIQUE,
body TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
Existing types to use:
pub struct AuthUser(pub User);
pub struct AppState { pub pool: PgPool }
pub enum AppError { Validation(String), Conflict, Internal(anyhow::Error) }
Do not use unwrap(). Use ? for error propagation.
Generate the slug from the title using a slugify function (assume it exists).
With this level of context, the model can generate handler code that slots directly into the existing codebase.
Patterns for Common Tasks
Generating Functions
Always specify:
- Exact function signature (name, parameters, return type)
- What "success" looks like
- What error conditions exist and how to handle them
Write a function with this signature:
fn validate_email(email: &str) -> Result<(), ValidationError>
Rules:
- Must contain exactly one @ symbol
- Local part: max 64 chars, alphanumeric plus . _ % + -
- Domain: valid hostname format, must contain a dot
- Total length: max 254 chars
- Return Err(ValidationError::InvalidEmail) for any violation
Refactoring Existing Code
Paste the code and be explicit about what to change and what to preserve:
Refactor this function to:
1. Replace the nested if/else with early returns
2. Extract the database query into a separate function named `fetch_user_by_email`
3. Add tracing::instrument attribute
4. Do NOT change the function signature or return type
5. Do NOT change error handling behavior
[paste code here]
Debugging
Provide the code, the error, and the context:
This Rust code panics at runtime. Identify the cause and provide a fix.
Do not change the function signature.
Error:
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 3'
at src/main.rs:42
Code:
[paste code]
Context: This function is called with user-provided input that may be empty.
Writing Tests
Specify the test framework, what cases to cover, and whether you want table-driven tests:
Write tests for the validate_email function using Rust's built-in test framework.
Cover:
- Valid email addresses (at least 3 examples)
- Missing @ symbol
- Multiple @ symbols
- Domain without a dot
- Local part exceeding 64 chars
- Empty string
- Email exceeding 254 chars total
Use table-driven tests (a Vec of test cases) to minimize repetition.
Code Review
Review this code for:
1. Correctness — does it do what the docstring claims?
2. Security — SQL injection, input validation, auth checks
3. Performance — N+1 queries, unnecessary allocations, missing indices
4. Rust idioms — use of unwrap/expect, error handling patterns
For each issue, provide:
- Severity: critical / major / minor
- Location: line number or function name
- Explanation: why it's a problem
- Fix: concrete code suggestion
[paste code]
Managing Context Windows for Large Codebases
For larger tasks, manage what you include:
- Include signatures, not full implementations of adjacent files — the model needs the interface, not the internals.
- Include relevant types — struct definitions and enums that the generated code will use.
- Include error types — so generated code can return the right errors.
- Exclude test files unless you're working on tests — they consume tokens without adding context.
# Good context package for a new handler:
- Existing handler (same file) for format reference: 30 lines
- AppState struct definition: 5 lines
- AuthUser extractor: 10 lines
- AppError enum: 15 lines
- Relevant DB function signatures: 10 lines
Total: ~70 lines of context → high signal, low noise
Iterative Generation
For complex code, generate incrementally rather than asking for everything at once:
- First pass — generate the skeleton/structure with TODOs
- Second pass — implement each TODO one by one
- Third pass — add error handling
- Fourth pass — add tests
This catches misunderstandings early and keeps each generation task small enough to verify.
Anti-Patterns to Avoid
- "Write a [application type]" — too broad; generates boilerplate you'll throw away
- No type information — forces the model to guess your data model
- Missing error handling requirements — generates happy-path-only code
- "Make it better" — undefined; say exactly what "better" means
- Asking for too much at once — 500-line requests rarely come back correct end-to-end
Key Takeaways
- Eliminate guesses: specify language, framework, types, constraints, and error handling explicitly
- Paste relevant existing code — the model needs to match your style and interfaces
- Specify test requirements upfront — getting tests written afterward is harder
- Generate incrementally for complex tasks
- Code review requests need explicit criteria to produce actionable feedback