Skip to content

Prompt Engineering for Developer Productivity

Practical prompt engineering techniques that turn LLMs into reliable coding assistants — covering context management, chain-of-thought, and structured outputs.

5 min read
Terminal showing a well-structured prompt generating accurate code output

Large language models are useful for coding tasks, but their output quality varies wildly based on how you ask. Vague prompts produce vague answers. Precise prompts with context, constraints, and examples produce code you can actually use. The difference is not the model — it is the prompt.

This is not about tricks or jailbreaks. It is about structuring your requests so the model has enough information to give useful answers on the first try.

The Anatomy of a Good Prompt

Every effective coding prompt has four components: context, task, constraints, and format.

markdownmarkdown
# ❌ Vague prompt — produces generic, possibly wrong code
"Write a function to validate emails"
 
# ✅ Structured prompt — produces usable, specific code
Context: TypeScript project using Zod for validation.
The function is part of a user registration form handler.
 
Task: Write a function that validates an email address.
 
Constraints:
- Must use Zod schema validation
- Must check for disposable email domains (mailinator, tempmail, guerrillamail)
- Must return a typed result object, not throw errors
- Must handle null/undefined input
 
Format: TypeScript function with JSDoc, followed by unit test examples.

The structured version tells the model exactly what stack to use, what edge cases to handle, and what the output should look like. The vague version forces the model to guess at every decision.

Context Window Management

Models have limited context. Dumping your entire codebase into the prompt wastes tokens on irrelevant code. Instead, provide targeted context — the specific types, interfaces, and patterns the generated code must integrate with.

tstypescript
// Include only the interfaces the generated code needs to implement
// Instead of pasting 500 lines of code, paste 30 lines of types
 
/*
Given these existing types and the database schema:
 
interface User {
  id: string;
  email: string;
  role: 'admin' | 'user' | 'moderator';
  createdAt: Date;
}
 
interface PaginatedResponse<T> {
  data: T[];
  total: number;
  page: number;
  pageSize: number;
  hasMore: boolean;
}
 
Database: PostgreSQL with Prisma ORM
Table: users (id UUID PK, email VARCHAR UNIQUE, role VARCHAR, created_at TIMESTAMP)
 
Write a function that queries users with pagination, filtering by role,
and returns a PaginatedResponse<User>.
*/
tstypescript
// The model can now produce code that matches your exact types
async function getUsers(
  params: { role?: User['role']; page: number; pageSize: number }
): Promise<PaginatedResponse<User>> {
  const { role, page, pageSize } = params;
  const skip = (page - 1) * pageSize;
 
  const where = role ? { role } : {};
 
  const [data, total] = await Promise.all([
    prisma.user.findMany({ where, skip, take: pageSize, orderBy: { createdAt: 'desc' } }),
    prisma.user.count({ where }),
  ]);
 
  return {
    data,
    total,
    page,
    pageSize,
    hasMore: skip + data.length < total,
  };
}

Chain-of-Thought for Complex Logic

For multi-step algorithms or architectural decisions, ask the model to reason through the problem before writing code.

markdownmarkdown
# ❌ Direct request — model jumps to code, may miss edge cases
"Implement a rate limiter using the sliding window algorithm"
 
# ✅ Chain-of-thought prompt
Task: Implement a sliding window rate limiter.
 
Before writing code, explain:
1. How the sliding window algorithm differs from fixed window
2. What data structure you'll use to track request timestamps
3. How you'll handle the window boundary calculation
4. What happens when the limit is exceeded
 
Then implement in TypeScript with:
- Redis for storage (using ioredis)
- Configurable window size and request limit
- Return type that includes remaining requests and reset time
tstypescript
// The model's reasoning leads to more robust implementations
// because it explicitly considers edge cases before coding
 
interface RateLimitResult {
  allowed: boolean;
  remaining: number;
  resetAt: Date;
  retryAfter?: number;  // seconds until next allowed request
}
 
async function checkRateLimit(
  redis: Redis,
  key: string,
  limit: number,
  windowMs: number
): Promise<RateLimitResult> {
  const now = Date.now();
  const windowStart = now - windowMs;
 
  // Remove entries outside the current window
  // Count entries within the window
  // Add current request if under limit
  const pipeline = redis.pipeline();
  pipeline.zremrangebyscore(key, 0, windowStart);
  pipeline.zcard(key);
  pipeline.zadd(key, now.toString(), `${now}-${Math.random()}`);
  pipeline.pexpire(key, windowMs);
 
  const results = await pipeline.exec();
  const currentCount = results?.[1]?.[1] as number;
 
  if (currentCount >= limit) {
    // Over limit — remove the entry we just added
    await redis.zremrangebyscore(key, now, now);
    const oldestInWindow = await redis.zrange(key, 0, 0, 'WITHSCORES');
    const resetAt = oldestInWindow.length > 1
      ? parseInt(oldestInWindow[1]) + windowMs
      : now + windowMs;
 
    return {
      allowed: false,
      remaining: 0,
      resetAt: new Date(resetAt),
      retryAfter: Math.ceil((resetAt - now) / 1000),
    };
  }
 
  return {
    allowed: true,
    remaining: limit - currentCount - 1,
    resetAt: new Date(now + windowMs),
  };
}

Few-Shot Examples

When the model needs to follow a specific pattern, provide examples of the input-output mapping. Two or three examples establish the pattern more reliably than lengthy descriptions.

markdownmarkdown
Task: Generate TypeScript API route handlers following this project's pattern.
 
Example 1:
Input: GET /api/users — list all users with pagination
Output:
tstypescript
// app/api/users/route.ts
export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const page = parseInt(searchParams.get('page') || '1');
  const limit = parseInt(searchParams.get('limit') || '20');
 
  const users = await db.user.findMany({
    skip: (page - 1) * limit,
    take: limit,
  });
 
  return NextResponse.json({ data: users, page, limit });
}
markdownmarkdown
Example 2:
Input: POST /api/users — create a new user
Output:
tstypescript
// app/api/users/route.ts
export async function POST(request: NextRequest) {
  const body = await request.json();
  const parsed = createUserSchema.safeParse(body);
 
  if (!parsed.success) {
    return NextResponse.json(
      { error: parsed.error.flatten() },
      { status: 400 }
    );
  }
 
  const user = await db.user.create({ data: parsed.data });
  return NextResponse.json({ data: user }, { status: 201 });
}
markdownmarkdown
Now generate:
Input: DELETE /api/users/[id] — delete a user by ID

The model now understands your project's conventions: Next.js route handlers, Zod validation, Prisma ORM, consistent response format. It follows the pattern without you specifying each decision.

Structured Output Requests

When you need the model to output data in a specific format — not just code — be explicit about the structure.

markdownmarkdown
# ❌ Ambiguous output format
"What are the security issues in this code?"
 
# ✅ Structured output format
Analyze this code for security vulnerabilities.
 
Output format (JSON):
{
  "vulnerabilities": [
    {
      "severity": "critical" | "high" | "medium" | "low",
      "line": number,
      "issue": "brief description",
      "fix": "code snippet showing the fix",
      "cwe": "CWE-XXX reference"
    }
  ],
  "summary": "one paragraph overall assessment"
}
jsonjson
// Model output follows the exact schema
{
  "vulnerabilities": [
    {
      "severity": "critical",
      "line": 15,
      "issue": "SQL query built with string concatenation allows injection",
      "fix": "db.query('SELECT * FROM users WHERE id = $1', [userId])",
      "cwe": "CWE-89"
    },
    {
      "severity": "high",
      "line": 28,
      "issue": "User input rendered without sanitization enables XSS",
      "fix": "import DOMPurify from 'dompurify'; element.innerHTML = DOMPurify.sanitize(input);",
      "cwe": "CWE-79"
    }
  ],
  "summary": "Two vulnerabilities found: a critical SQL injection and a high-severity XSS. Both stem from unsanitized user input being used directly in queries and DOM manipulation."
}

Iterative Refinement

Single-shot prompts rarely produce production-ready code. Treat the model as a pair programmer — give feedback, ask for modifications, narrow in on the right solution.

markdownmarkdown
# Round 1: Get the basic structure
"Implement a retry mechanism with exponential backoff for HTTP requests"
 
# Round 2: Add constraints based on the output
"Good. Now modify it to:
- Use a jitter factor to prevent thundering herd
- Accept a custom shouldRetry predicate
- Log each retry attempt with the attempt number and delay
- Respect a maximum total timeout, not just max retries"
 
# Round 3: Integration
"Now wrap this in a class that can be used as middleware
for our existing HttpClient. Show usage examples."

Each round builds on the previous output. The model has the full conversation context, so it refines rather than starts over. This is faster than trying to specify everything perfectly in one shot.

Prompt Templates for Recurring Tasks

For tasks you perform repeatedly, save prompt templates that include your project's conventions.

markdownmarkdown
## Bug Fix Template
 
Context: [paste the error message and stack trace]
 
Relevant code: [paste the function/file where the error occurs]
 
Expected behavior: [what should happen]
Actual behavior: [what actually happens]
 
Constraints:
- Fix must not change the public API
- Must include a test case that would have caught this bug
- Explain why the bug occurred (root cause)
 
## Code Review Template
 
Review this code for:
1. Logic errors or edge cases
2. Performance issues (N+1 queries, unnecessary re-renders)
3. Security vulnerabilities
4. Deviation from project conventions
 
For each issue found, provide:
- Line number
- Issue description
- Suggested fix with code

These templates turn LLMs from inconsistent chat tools into reliable development assistants. The consistency comes from the prompt structure, not from the model's training.

Key Takeaways

  1. Structure prompts with context, task, constraints, and format — vague prompts produce vague code
  2. Provide targeted context, not entire files — types and interfaces give the model what it needs to integrate with your codebase
  3. Use chain-of-thought for complex logic — reasoning before coding catches edge cases
  4. Few-shot examples establish patterns — two examples are worth more than a paragraph of description
  5. Iterate in rounds — treat the model as a pair programmer, not an oracle
  6. Save prompt templates — recurring tasks get consistent results with reusable structures
Wilfredo Rujel

Wilfredo Rujel

Full Stack Software Engineer

Share this postX