top of page

Tutorial 6: Prompting for Code Generation

  • Contributor
  • 4 days ago
  • 3 min read

LLMs write impressive code in demos. Production code generation needs more discipline. This tutorial walks through the patterns.

What You'll Build

Code generation prompts that produce working, testable, debuggable code.

Step 1: Specify the Context (15 min)

The model needs to know:

  • Language and version

  • Framework and version

  • Code style preferences

  • Existing patterns in the codebase

  • What it's integrating with

You are writing TypeScript for a Node.js 20 backend using Express 4.18.

Code style:
- ES modules (import/export)
- Async/await over .then()
- Named exports preferred
- Functional style where natural
- Comments only for non-obvious logic

The code integrates with our existing user service (signatures shown below).

Generic prompts produce generic code. Specific context produces fitting code.

Step 2: Show Examples of the Codebase (15 min)

Include 1-2 existing functions:

Here are examples of how we structure handlers:

import { Request, Response } from 'express';
import { userService } from '../services/userService.js';

export const getUserHandler = async (req: Request, res: Response) => {
  const userId = req.params.id;
  
  const user = await userService.findById(userId);
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  
  return res.status(200).json(user);
};

The model patterns its output. Style consistency improves dramatically.

Step 3: Be Specific About the Task (10 min)

Bad: "Write a function to handle user updates."

Good: "Write an Express handler for PATCH /api/users/:id. It should:

  • Validate the request body matches the UserUpdate schema

  • Return 400 if validation fails

  • Return 404 if user doesn't exist

  • Return 200 with the updated user on success

  • Use the existing userService.update(id, updates) method

  • Handle errors with the existing errorHandler middleware"

Detailed prompts produce detailed code.

Step 4: Specify the Tests (15 min)

Ask for tests too:

Also write tests for this handler using vitest and supertest. 
Cover:
- Successful update (200)
- Validation failure (400)
- Non-existent user (404)
- Authentication required (401)

Tests come for free. Often the model writes good tests; verify they work.

Step 5: Validate the Output (15 min)

LLMs hallucinate. For code:

  • Run it. Does it execute?

  • Does it type-check?

  • Does it pass tests?

  • Does it actually solve the stated problem?

Each is a check. Don't trust output you haven't verified.

Step 6: Iterate on Failures (varies)

When generated code is wrong:

  • Paste the actual error back

  • Ask for the specific fix

  • Don't just regenerate from scratch

The code you wrote has this TypeScript error:
"Property 'id' does not exist on type 'Request<ParamsDictionary>'"

Fix this specific error.

Targeted iteration is faster than starting over.

Step 7: Watch for Common Patterns to Avoid (10 min)

LLM-generated code often has:

  • Outdated patterns (Node.js v12 patterns when you're on v20)

  • Made-up library APIs (functions that don't exist)

  • Insecure patterns (SQL injection, XSS)

  • Over-defensive code (try/catch everything; null checks for impossible cases)

  • Inconsistent style with your codebase

Each is a thing to catch in review.

Step 8: Use for Boilerplate, Not Architecture (5 min)

LLMs are best at:

  • CRUD handlers

  • Test scaffolding

  • Data transformation

  • Validation

  • Refactoring (when you tell them what to refactor)

LLMs are worst at:

  • System design decisions

  • Non-obvious business logic

  • Performance optimization

  • Cross-cutting concerns (auth, observability)

Use LLMs for the easy stuff; reserve human time for the hard.

Step 9: Refactoring With LLMs (15 min)

Refactoring tasks where LLMs help:

Refactor this function to:
- Use Result<T, Error> return type instead of throwing
- Add type annotations
- Extract the validation logic into a separate function
- Maintain identical behavior (don't change the logic)

[function code]

Specific refactoring requests produce useful outputs. Vague "improve this code" produces unpredictable changes.

Step 10: Review Carefully (always)

LLM-generated code passes type-checks and tests but still needs review:

  • Is it solving the right problem?

  • Does it match codebase conventions?

  • Is it secure?

  • Does it handle the edge cases?

  • Will the next engineer understand it?

Code that works isn't the same as code that's good. Review like any other PR.

What You Just Did

You set up prompts that produce usable code. With context, examples, specific tasks, and tests, the output quality rises dramatically.

Common Failure Modes

No context. Generic code that doesn't fit your codebase.

No verification. Code that "looks right" but doesn't actually run.

Trust without review. Hallucinations slip through.

No iteration. First output is final, even when wrong.

Architecture decisions to LLM. Hand the wrong question to the wrong tool.

bottom of page