codeintelligently
Back to posts
AI & Code Quality

How to Write Prompts That Produce Production-Quality Code

Vaibhav Verma
9 min read
aipromptingcode-qualitybest-practicesproductivityprompt-engineering

How to Write Prompts That Produce Production-Quality Code

I've reviewed over 500 AI-generated code snippets in the last year. The quality difference between good prompts and bad prompts is staggering. A well-crafted prompt produces code I'd approve in a PR with minor changes. A lazy prompt produces code that needs a complete rewrite.

Most prompting guides tell you to "be specific." That's true but useless. It's like telling a junior developer to "write good code." The question is how. Here's the specific, repeatable system I use to get production-quality code from AI on the first try, 80% of the time.

The Contrarian Take on AI Prompting

Here's what most people get wrong: they think the goal of a prompt is to describe what you want. It's not. The goal is to constrain what you don't want. AI generates the most common pattern by default. Your job is to eliminate the common patterns that don't fit your codebase.

I tracked prompt quality across my team for 4 months. Prompts that described desired behavior produced acceptable code 45% of the time. Prompts that described desired behavior plus constraints produced acceptable code 82% of the time. The constraints are what matter.

The CRAFT Prompt Framework

Every production code prompt should follow this structure:

C - Context (Your Codebase)

Tell the AI about your existing code. Reference specific files, patterns, and conventions.

Bad: "Write a user registration function"

Good: "Write a user registration function for our Express app.
We use:
- Prisma ORM with PostgreSQL
- Zod for input validation (schemas in /src/schemas/)
- Result<T, E> pattern for error handling (never throw)
- bcrypt for password hashing (12 rounds)
- Our mailer service at /src/services/mailer.ts for welcome emails"

The bad prompt produces generic code. The good prompt produces code that fits your stack. Every prompt for production code should reference at least 3 codebase-specific details. I call this the "Rule of 3 References."

R - Requirements (What It Must Do)

List the functional requirements as bullet points. Be explicit about edge cases.

Requirements:
- Validate email format and uniqueness
- Validate password: min 8 chars, 1 uppercase, 1 number
- Hash password before storing
- Create user record with default role "member"
- Send welcome email asynchronously (don't block registration)
- Return the user object without the password hash
- Handle duplicate email with specific error code USER_EXISTS

Notice I'm listing 7 requirements for a single function. That feels excessive until you realize that every requirement you don't specify, the AI will handle however it wants. And "however it wants" is usually wrong for your specific use case.

A - Anti-patterns (What It Must NOT Do)

This is the part most people skip and it's the most important part. Tell the AI what you don't want.

Do NOT:
- Use try/catch (use our Result pattern)
- Import any library not already in package.json
- Use console.log (use our logger from @/lib/logger)
- Return the password hash in any response
- Send the email synchronously
- Use any() type annotations

Anti-patterns cut the error rate in half. Without them, AI will import a new email library instead of using yours. It'll use try/catch instead of your Result pattern. It'll log to console instead of your structured logger.

F - Format (Code Structure)

Specify the exact structure you want.

Structure:
- Export a single async function named registerUser
- Parameter: input of type RegisterInput (define the Zod schema)
- Return type: Promise<Result<UserResponse, RegistrationError>>
- Put the Zod schema at the top of the file
- Put the main function below the schema
- No classes, no dependency injection

T - Tests (Expected Behavior)

Include the test cases. This is the cheat code nobody uses.

This function should pass these tests:
- Valid input creates user and returns success
- Duplicate email returns Result.err(USER_EXISTS)
- Invalid password format returns Result.err(INVALID_PASSWORD)
- Database failure returns Result.err(INTERNAL_ERROR)
- Welcome email failure doesn't affect registration result

When you include test cases in the prompt, AI generates code that handles those cases. Without test cases, it generates code that handles the happy path and maybe one error case.

Before and After: Real Examples

Example 1: API Endpoint

Lazy prompt:

Write an endpoint to update user settings

CRAFT prompt:

Write a PUT /api/users/:id/settings endpoint.

Context: Express app, Prisma ORM, Zod validation, authMiddleware
verifies JWT and sets req.user. Settings stored in UserSettings
table (one-to-one with User).

Requirements:
- User can only update their own settings (check req.user.id)
- Validate: theme (light/dark), language (en/es/fr/de),
  notifications (boolean), timezone (valid IANA string)
- Partial updates allowed (only update provided fields)
- Return updated settings object

Anti-patterns:
- No try/catch, use Result pattern
- No new dependencies
- Don't expose internal field names in error messages

Format:
- Router handler function, not standalone app
- Use settingsSchema.partial() for validation

Tests:
- Valid partial update returns updated settings
- Updating another user's settings returns 403
- Invalid timezone returns 400 with field-specific error
- Missing settings record creates one (upsert behavior)

The lazy prompt produces a generic settings endpoint with no auth, no validation, and full try/catch error handling. The CRAFT prompt produces an endpoint that matches your codebase. I timed this across 20 instances: the CRAFT prompt takes 3 minutes longer to write but saves 25 minutes of rework.

The Prompt Library Approach

Writing CRAFT prompts from scratch every time is tedious. So don't. Build a prompt library for your team.

typescript
// prompts/api-endpoint.md
// Template for generating API endpoints

// Context block (fill in once per project):
const PROJECT_CONTEXT = `
Express app with TypeScript.
ORM: Prisma with PostgreSQL.
Validation: Zod (schemas in /src/schemas/).
Auth: authMiddleware from @/middleware/auth.
Error handling: Result&#x3C;T, E> pattern.
Logger: structuredLogger from @/lib/logger.
HTTP client: httpClient from @/lib/http.
`;

// Per-endpoint template:
const ENDPOINT_PROMPT = `
${PROJECT_CONTEXT}

Write a [METHOD] [PATH] endpoint.

Requirements:
- [List specific requirements]
- [Include edge cases]

Anti-patterns:
- No try/catch (use Result pattern)
- No new dependencies
- No console.log (use structuredLogger)
- No any types

Format:
- Router handler exported from module
- Zod schema for request validation
- Type-safe response object

Tests this should pass:
- [Happy path]
- [Auth failure]
- [Validation failure]
- [Edge case]
`;

Share these templates across your team. When someone needs a new endpoint, they fill in the brackets. This standardizes AI output quality across the entire team without requiring everyone to be a prompting expert.

Measuring Prompt Quality

Track these metrics for your AI-generated code:

Metric Poor Prompts Good Prompts (CRAFT)
First-try acceptance rate 15-25% 70-85%
Lines changed in review 40-60% of generated code 5-15% of generated code
Pattern violations 3-5 per generation 0-1 per generation
Missing edge case handling 60-80% of cases missed 10-20% of cases missed
New dependency introductions 1-2 per generation 0 per generation

The numbers are clear. Spending 3-5 minutes on a CRAFT prompt saves 20-40 minutes of cleanup. That's a 4-8x return on time invested.

The One Thing That Matters Most

If you take one thing from this article, take this: specify anti-patterns. Tell AI what not to do. The single highest-impact change to prompt quality is adding a "Do NOT" section. It takes 60 seconds to write and eliminates the most common AI code quality issues.

Your prompts are your interface to AI. Invest in them the way you'd invest in any API contract. Define the inputs, outputs, constraints, and error cases. The AI will deliver.

$ ls ./related

Explore by topic