Prompt Engineering

Writing Prompts for Code Generation

Get better code from AI: specify language, constraints, architecture, and tests in your prompts. Covers patterns for generating, refactoring, debugging, and reviewing code.

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:

  1. Include signatures, not full implementations of adjacent files — the model needs the interface, not the internals.
  2. Include relevant types — struct definitions and enums that the generated code will use.
  3. Include error types — so generated code can return the right errors.
  4. 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:

  1. First pass — generate the skeleton/structure with TODOs
  2. Second pass — implement each TODO one by one
  3. Third pass — add error handling
  4. 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