MervCodes

Tech Reviews From A Programmer

Prompt Engineering for Developers: Write Better AI Prompts for Code Generation

8 min read

Most developers using AI coding tools get mediocre results because they write mediocre prompts. The difference between "write a function to validate email" and a well-structured prompt is the difference between getting a regex one-liner and getting a production-ready validator with edge case handling, error messages, and tests. This guide covers the techniques that actually improve code generation quality.

TL;DR: Practical prompt engineering techniques for coding tasks. Learn few-shot prompting, chain-of-thought debugging, and how to structure prompts for better code output.

Principle 1: Be Specific About Context

The most common mistake is assuming the AI knows your project context. It does not. Provide the relevant information explicitly.

Bad Prompt

Write a function to create a user.

This generates generic, context-free code that you will need to rewrite entirely.

Good Prompt

Write a TypeScript function `createUser` for a Next.js API route that:
- Takes name, email, and optional avatar URL
- Validates email format and checks for existing users in PostgreSQL (using Prisma)
- Hashes the password with bcrypt (12 rounds)
- Returns the created user without the password field
- Throws a typed error (UserExistsError) if the email is taken
- Follow the patterns in our existing codebase where we use Result<T, E> return types

The specific prompt produces code you can actually use. Every constraint you specify is a constraint the AI does not have to guess.

What to Include

  • Language and framework (TypeScript, Next.js, Express, etc.)
  • Libraries you use (Prisma, Drizzle, bcrypt — not generic alternatives)
  • Error handling approach (throw errors, return Result types, return null)
  • Naming conventions (camelCase, PascalCase for types, etc.)
  • Existing patterns ("follow the pattern in our existing auth code")

Principle 2: Few-Shot Prompting

Show the AI an example of what you want, then ask for more. This is dramatically more effective than describing the pattern in words.

For Consistent API Endpoints

Here's our existing GET endpoint pattern:

```typescript
// src/app/api/products/route.ts
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { withAuth } from '@/lib/auth';

export const GET = withAuth(async (req, { user }) => {
  const products = await db.product.findMany({
    where: { organizationId: user.orgId },
    orderBy: { createdAt: 'desc' },
  });
  return NextResponse.json({ products });
});

Now write POST and DELETE endpoints for the same resource following the exact same pattern. Include input validation with zod.


The AI will match your `withAuth` wrapper, your response format, your import paths, and your database query style because it has a concrete example to follow.

### For Test Files

Here's how we write tests in this project:

import { describe, it, expect, vi } from 'vitest';
import { createUser } from './create-user';
import { db } from '@/lib/db';

vi.mock('@/lib/db');

describe('createUser', () => {
  it('creates a user with valid input', async () => {
    const mockUser = { id: '1', name: 'Test', email: '[email protected]' };
    vi.mocked(db.user.create).mockResolvedValue(mockUser);

    const result = await createUser({ name: 'Test', email: '[email protected]' });
    expect(result).toEqual(mockUser);
  });
});

Write comprehensive tests for the updateUser function using the same patterns (vitest, vi.mock for db, describe/it structure). Cover: successful update, user not found, invalid email, concurrent modification.


## Principle 3: Chain-of-Thought for Debugging

When debugging, do not just paste the error. Walk the AI through the problem systematically.

### Bad Debugging Prompt

I get this error: TypeError: Cannot read properties of undefined (reading 'map') Fix it.


### Good Debugging Prompt

I'm getting a TypeError in my React component. Here's the context:

Error: TypeError: Cannot read properties of undefined (reading 'map') File: src/components/UserList.tsx, line 14 When it happens: On initial page load, but works after navigation

Relevant code:

function UserList() {
  const { data } = useQuery({ queryKey: ['users'], queryFn: fetchUsers });
  return (
    <ul>
      {data.users.map((user) => (  // Line 14: error here
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

What I've checked:

  • The API endpoint returns { users: [...] } correctly
  • The error only happens on cold page loads, not client-side navigation
  • No error in the network tab

Walk through the possible causes step by step and suggest a fix.


The structured context lets the AI immediately identify this as a loading state issue (data is undefined before the query resolves) rather than guessing from a bare error message.

## Principle 4: Structured Output Requests

When you need the AI to generate something specific, define the output format:

Analyze this database schema and generate a migration plan.

Output as a numbered list where each step includes:

  1. The SQL statement
  2. Whether it's reversible (yes/no)
  3. Estimated downtime impact (none/brief/extended)
  4. Any data backfill needed

Schema changes:

  • Add status enum column to orders table (values: pending, shipped, delivered)
  • Add index on orders.customer_id
  • Rename orders.total to orders.total_cents (integer, was decimal)
  • Drop unused orders.legacy_ref column

## Principle 5: Iterative Refinement

Do not try to get everything in one prompt. Build incrementally:

Step 1: "Write the type definitions for a shopping cart: Cart, CartItem, CartAction (add, remove, update quantity, clear)"

Step 2: "Now write a useReducer-based React hook useCart that implements these actions. Use the types from step 1."

Step 3: "Add localStorage persistence to the useCart hook. Save on every state change, load on initialization. Handle the case where stored data is corrupted or has an outdated schema."

Step 4: "Write tests for the useCart hook covering all actions, persistence, and the corrupted data edge case."


Each step builds on the previous one. The AI maintains context and produces better results than trying to generate everything at once.

## Technique: Constraints-First Prompting

List what the code should NOT do before what it should do. Constraints are more powerful than instructions:

Write a rate limiter middleware for Express.

Constraints:

  • Do NOT use any external libraries (no express-rate-limit)
  • Do NOT store rate limit data in-memory (use Redis)
  • Do NOT block the event loop (all operations must be async)
  • Do NOT trust X-Forwarded-For without explicit configuration

Requirements:

  • Sliding window algorithm
  • Configurable window size and max requests
  • Return standard 429 with Retry-After header
  • Support custom key extraction (IP, API key, user ID)

Constraints prevent the AI from taking shortcuts (like suggesting a library) and force it to generate the implementation you actually need.

## Technique: Role-Based Prompting

Assign a specific role to get domain-appropriate responses:

You are a database performance engineer reviewing slow queries.

This query takes 3.2 seconds on a table with 2M rows:

SELECT u.*, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.created_at > '2025-01-01'
GROUP BY u.id
ORDER BY order_count DESC
LIMIT 20;

Analyze the query plan, suggest index additions, and rewrite the query if the structure itself is problematic. Explain the performance implications of each suggestion.


## Anti-Patterns to Avoid

### 1. The Kitchen Sink Prompt

Do not ask for everything at once:

// TOO MUCH Write a complete authentication system with login, register, forgot password, email verification, OAuth with Google and GitHub, role-based access control, session management, CSRF protection, rate limiting, and audit logging.


Break this into focused requests. Authentication alone has dozens of components.

### 2. The Vague Refactor

// TOO VAGUE Make this code better.


Instead: "Refactor this function to separate the validation logic from the business logic. Extract the database queries into a repository pattern. Add error handling for the three failure modes: invalid input, duplicate entry, and connection timeout."

### 3. Ignoring the Output

Read the generated code critically. AI code often:
- Uses deprecated APIs
- Misses error handling for edge cases
- Imports packages you do not use
- Uses different patterns than your codebase

Always review, test, and adapt. AI-generated code is a starting point, not a final product.

## The 80/20 of Prompt Engineering

If you remember nothing else, remember these four rules:

1. **Specify the language, framework, and libraries** — "TypeScript with Next.js App Router and Prisma"
2. **Show an example** of the pattern you want followed
3. **List constraints** before requirements
4. **Break complex tasks** into sequential steps

These four techniques cover 80% of prompt engineering value. The remaining 20% is context-specific refinement that you develop through practice.

## Key Takeaways

- Specific prompts with context produce dramatically better code than vague requests
- Few-shot prompting (showing an example) is the single most effective technique
- Structure debugging prompts with error, context, code, and what you have already checked
- Use constraints to prevent the AI from taking shortcuts
- Build incrementally — four focused prompts beat one massive prompt
- Always review AI output critically — it is a draft, not production code


## Sources

1. [Anthropic Prompt Engineering Guide](https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering)
2. [Anthropic Documentation](https://docs.anthropic.com/)
3. [OpenAI Documentation](https://platform.openai.com/docs)

---

*Looking for more? Check out [Adaptels](https://adaptels.com).*

Related Articles

How to Debug Node.js Memory Leaks (Step-by-Step Guide)

Learn how to detect, diagnose, and fix Node.js memory leaks using heap snapshots, Chrome DevTools, and clinic.js — with real code examples.

Running Local LLMs With Ollama: Developer Setup Guide

Set up Ollama to run local LLMs on your machine. Covers installation, model selection, API usage, and integrating local models into your dev workflow.

AI Code Review Tools for Developers: Automate Your PR Reviews (2026)

Compare the best AI code review tools in 2026 — CodeRabbit, GitHub Copilot, Sourcery, and more. Setup guides, pricing, and real-world recommendations.