Prompt Patterns & Templates
Learning Outcomes
- Apply the scaffold pattern to break large tasks into structured phases
- Use the critic pattern to generate-then-review for higher quality output
- Iterate toward quality using the refine pattern
- Convert between formats, languages, and paradigms with the transform pattern
- Build and organise a personal prompt template library
Lesson Plan
| Segment | Duration | Topic |
|---|---|---|
| Intro | 3 min | Why prompt patterns matter |
| Demo | 10 min | The scaffold pattern |
| Demo | 10 min | The critic pattern |
| Demo | 8 min | The refine pattern |
| Demo | 8 min | The transform pattern |
| Explain | 6 min | Building your prompt library |
| Demo | 3 min | Organising and sharing templates |
| Wrap-up | 2 min | Key takeaways |
Before You Begin
Pre-work:
- Complete Lesson 8 on Multi-Turn Conversations
- Identify 2-3 tasks you do repeatedly when coding
- Think about what "good output" looks like for those tasks
Shopping List:
- An AI coding tool (Claude Code, Cursor, Codex, or similar)
- A project to practice patterns on
- A place to save your templates (notes app, repo file, or dedicated document)
- Familiarity with your most common development workflows
The scaffold pattern separates structure from content. First you ask the AI to create the skeleton, then you fill in the details piece by piece.
Why it works: Large tasks overwhelm AI when attempted all at once. By scaffolding first, you establish structure that guides the detailed work.
The basic scaffold flow:
Step 1: Generate structure only (no implementation)
Step 2: Review and adjust the structure
Step 3: Fill in one section at a time
Step 4: Review the whole
Example: Scaffolding an API module
Step 1 — Generate structure:
> I need a complete user management module.
> First, just give me the file structure and key interfaces —
> no implementation yet.
>
> The module needs: CRUD operations, role-based access,
> password management, and activity logging.
AI produces:
src/users/
├── user.types.ts (User, CreateUserDTO, UpdateUserDTO)
├── user.controller.ts (CRUD endpoints + role assignment)
├── user.service.ts (business logic + password hashing)
├── user.repository.ts (database queries)
├── user.middleware.ts (auth checks, role guards)
├── user.validator.ts (Zod schemas for input)
└── user.test.ts (integration tests)
Key interfaces:
- User { id, email, name, role, createdAt, updatedAt }
- CreateUserDTO { email, name, password, role? }
- UpdateUserDTO { name?, email?, role? }
Step 2 — Adjust:
> Good structure. Two changes:
> - Split tests into user.unit.test.ts and user.integration.test.ts
> - Add user.events.ts for activity logging events
> Now implement user.types.ts
Step 3 — Fill in piece by piece:
> Now implement user.repository.ts using the types we defined
> Now user.service.ts — it should call the repository and handle
> the password hashing and role validation logic
Example: Scaffolding a complex function
> I need a function that processes a CSV upload: validates format,
> parses rows, transforms data, handles errors per-row, and returns
> a summary.
>
> First, write just the function signature, the main steps as comments,
> and the return type. No implementation yet.
AI produces:
interface ProcessResult {
successful: number;
failed: number;
errors: Array<{ row: number; field: string; message: string }>;
data: TransformedRecord[];
}
async function processCSVUpload(
file: Buffer,
schema: ValidationSchema
): Promise<ProcessResult> {
// Step 1: Validate file format (encoding, headers, size)
// Step 2: Parse CSV into raw rows
// Step 3: Validate each row against schema
// Step 4: Transform valid rows into target format
// Step 5: Collect errors from invalid rows
// Step 6: Return summary with results and errors
}
Then you fill each step:
> Implement step 1 and 2 — file validation and CSV parsing
When to use the scaffold pattern:
- Building a new module or feature from scratch
- Creating complex functions with many steps
- Writing documentation with multiple sections
- Generating test suites (scaffold all test cases, then implement)
- Any task where getting the structure right is half the battle
The critic pattern has two phases: generate, then critically review. Instead of hoping for perfect output on the first try, you explicitly ask the AI to evaluate its own work and fix issues.
Why it works: AI is often better at spotting problems in existing code than avoiding them during generation. The critic phase catches issues the generation phase missed.
The basic critic flow:
Step 1: Generate the code/solution
Step 2: "Now review what you just wrote — what's wrong with it?"
Step 3: "Fix those issues"
Example: Generating and critiquing an API endpoint
Step 1 — Generate:
> Write a POST /api/orders endpoint that creates an order from
> a shopping cart. Include inventory check and payment processing.
Step 2 — Critique:
> Now review the code you just wrote. Look for:
> - Race conditions (what if two users buy the last item?)
> - Error handling gaps (what if payment succeeds but DB write fails?)
> - Security issues (input validation, authorisation)
> - Performance concerns (N+1 queries, unnecessary DB calls)
>
> List every issue you find.
AI responds with issues:
Issues found:
1. Race condition: No lock on inventory check → two orders could
claim the same item
2. No transaction: if payment succeeds but order insert fails,
money is charged without an order record
3. Missing auth check: any user could create an order for any cart
4. N+1: loading each cart item's product info individually
5. No idempotency key: retrying a failed request could double-charge
Step 3 — Fix:
> Fix all 5 issues. Use a database transaction, optimistic locking
> for inventory, and add an idempotency check.
The "devil's advocate" variation:
> You proposed using Redis for our session store. Now argue against
> that choice. What could go wrong? What are we not considering?
> What would a sceptic say?
This forces the AI to stress-test its own recommendations.
The "senior review" variation:
> Pretend you're a senior engineer reviewing this code in a PR.
> What comments would you leave? Be critical — I want to improve
> this before it ships.
The "security audit" variation:
> Assume a malicious user is looking at this endpoint.
> What would they try? What inputs would they send?
> Where are the weak points?
Chaining critic passes:
For important code, run multiple critic passes with different lenses:
Pass 1: "Review for correctness — any logic bugs?"
Pass 2: "Review for security — any vulnerabilities?"
Pass 3: "Review for performance — any bottlenecks?"
Pass 4: "Review for maintainability — what's hard to understand or change?"
Each pass catches different categories of issues.
The refine pattern starts with a rough first attempt and iteratively improves it through targeted feedback. Instead of trying to get perfection in one shot, you shape the output through successive passes.
Why it works: It's easier to improve something that exists than to specify everything perfectly upfront. Each refinement pass addresses one concern, keeping changes focused and controllable.
The basic refine flow:
Step 1: Generate a first draft (accepting imperfection)
Step 2: Identify the most important improvement
Step 3: Request that specific improvement
Step 4: Repeat until satisfied
Example: Refining a function
Draft:
> Write a function that searches users by name, email, or role.
> Just get something working, don't worry about perfection.
Refine pass 1 — Performance:
> Good start. Now optimise: the search should be case-insensitive
> and support partial matching without loading all users into memory.
Refine pass 2 — Edge cases:
> Now handle edge cases: empty search string, special characters
> in the query, and what happens when the database is unavailable.
Refine pass 3 — Interface:
> Clean up the interface: add pagination support, allow combining
> filters (name AND role), and return the total count alongside results.
Each pass makes the function better while keeping changes scoped.
Example: Refining an error message
Draft: "Write user-facing error messages for our form validation"
Refine 1: "Make them friendlier — less technical, more conversational"
Refine 2: "Add specific fix suggestions for each error"
Refine 3: "Keep them under 100 characters each for mobile display"
The "dimension" refinement technique:
Name specific dimensions to improve along:
> Here's my current code. Refine it for readability —
> I want a junior developer to understand it without comments.
> Now refine for testability — I should be able to unit test
> each part in isolation without mocking the universe.
> Finally, refine for error handling — every failure mode
> should produce an actionable error message.
Example: Refining documentation
Draft: "Write API docs for the /tasks endpoints"
Refine 1: "Add request/response examples for each endpoint"
Refine 2: "Include error responses — what does a 400 look like vs 404 vs 500?"
Refine 3: "Add a 'common mistakes' section based on the validations we have"
When to stop refining:
- When the output meets your quality bar
- When refinements start contradicting earlier ones
- When you're polishing for marginal gains (diminishing returns)
- When you've been refining for more passes than it would take to just edit manually
The "one thing at a time" rule:
Each refinement pass should target ONE dimension:
- Good: "Make it more performant"
- Bad: "Make it more performant, add error handling, rename variables, and add types"
Multiple concerns in one refinement pass leads to confusion and regression.
The transform pattern converts existing code or content from one form to another. It leverages the AI's ability to understand structure and re-express it differently.
Why it works: Transformation tasks have clear input and output, making them highly predictable. The AI can maintain semantics while changing syntax, structure, or paradigm.
Common transformations:
| From | To | Example |
|---|---|---|
| JavaScript | TypeScript | Add types to existing JS code |
| REST API | GraphQL | Convert endpoints to schema + resolvers |
| Class-based | Functional | React classes to hooks |
| Callbacks | Async/Await | Modernise legacy patterns |
| SQL | ORM queries | Raw SQL to Knex/Prisma |
| One framework | Another | Express routes to Fastify |
| Config format | Another | YAML to JSON, JSON to TOML |
| Imperative | Declarative | Loops to map/filter/reduce |
| Test framework | Another | Mocha to Jest |
Example: JavaScript to TypeScript
> Transform this JavaScript module to TypeScript.
> Add proper types (no 'any'). Infer types where obvious,
> create interfaces where the shape is complex.
> Keep the logic identical — only add types.
>
> [paste JS code]
Example: Converting paradigms
> Convert this class-based React component to a functional component
> with hooks. Map lifecycle methods as follows:
> - componentDidMount → useEffect with empty deps
> - componentDidUpdate → useEffect with relevant deps
> - componentWillUnmount → useEffect cleanup
> - this.state → useState
> - this.props → function parameters
>
> Preserve all behaviour. Don't "improve" the logic — just transform it.
Example: Converting between formats
> Convert this Express route configuration to an OpenAPI 3.0 spec:
> - Extract path parameters, query parameters, and body schemas
> - Infer response types from the handler return values
> - Add appropriate status codes based on the error handling
>
> [paste Express routes]
The "preserve semantics" instruction:
Always be explicit about whether you want pure transformation or transformation plus improvement:
Pure transformation:
> Convert this to TypeScript. Don't change any logic, algorithms,
> or structure. Only add types.
Transformation plus improvement:
> Convert this to TypeScript and also:
> - Replace 'any' with proper types
> - Extract magic strings to constants
> - Add null checks where the types reveal potential nulls
Example: Transforming between test styles
> Transform these Mocha/Chai tests to Jest syntax:
> - describe/it blocks stay the same
> - expect(x).to.equal(y) → expect(x).toBe(y)
> - expect(x).to.deep.equal(y) → expect(x).toEqual(y)
> - before/after → beforeAll/afterAll
> - beforeEach/afterEach stay the same
> - sinon stubs → jest.fn()
>
> Keep test descriptions and assertions identical.
Batch transformations:
> Apply this transformation to all files in src/controllers/:
> - Add return type annotations to all controller methods
> - Wrap response sends in a standard formatResponse() helper
> - Add request validation using the matching schema from src/schemas/
The best prompts deserve to be reused. Here's how to build and organise a personal library of prompt templates.
What belongs in your library:
- Prompts you've used more than 3 times
- Prompts that took multiple iterations to get right
- Prompts that consistently produce good output
- Prompts for your most common tasks
Template format:
Create a consistent format for your saved prompts:
## Template: [Name]
**When to use:** [Situation description]
**Pattern:** [scaffold/critic/refine/transform/other]
**Works best with:** [types of tasks]
**Template:**
> [The actual prompt with [PLACEHOLDERS] for variable parts]
**Variables:**
- [PLACEHOLDER]: what to fill in here
**Example:**
> [A concrete example of the template in use]
**Tips:**
- [Things that make this template work better]
Example library entries:
## Template: New Feature Scaffold
**When to use:** Starting a new feature from scratch
**Pattern:** Scaffold
**Template:**
> I need to build [FEATURE_DESCRIPTION].
> Tech stack: [STACK]
>
> First, give me:
> 1. File structure (what files to create)
> 2. Key interfaces/types
> 3. Implementation order (dependencies)
>
> Don't implement anything yet — just the skeleton.
---
## Template: Code Critic
**When to use:** After generating complex or security-sensitive code
**Pattern:** Critic
**Template:**
> Review the [CODE_TYPE] you just wrote. Check for:
> - [CONCERN_1]
> - [CONCERN_2]
> - [CONCERN_3]
> - Any issues I haven't thought of
>
> List every problem, then fix them all.
---
## Template: Legacy Modernisation
**When to use:** Upgrading old code patterns
**Pattern:** Transform
**Template:**
> Transform this [OLD_PATTERN] code to [NEW_PATTERN].
> Rules:
> - Preserve all behaviour (same inputs → same outputs)
> - Don't change public interfaces
> - Flag anything that can't be directly transformed
>
> [PASTE CODE]
Where to store your library:
| Option | Best For |
|---|---|
| Markdown file in your dotfiles repo | Personal use, version-controlled |
| Notion/notes app | Easy search, tagging, mobile access |
| Project repo (docs/prompts/) | Team-shared, project-specific |
| Snippets tool (Alfred, Raycast) | Quick access during coding |
| Rules file | Patterns you want AI to apply automatically |
~/prompts.md). One file, no folders, no tooling. Upgrade to something fancier only when you outgrow it — most people never need more than a well-organised markdown file.Growing your library:
- Start with 5 templates — your most repeated tasks
- Add one per week — when you craft a prompt that works unusually well
- Prune quarterly — remove templates you haven't used in 3 months
- Refine continuously — update templates as you discover improvements
Combining patterns:
Your most powerful templates often combine multiple patterns:
Scaffold + Critic:
"Create the structure, then review it for gaps before implementing"
Generate + Refine + Transform:
"Write it in pseudocode, refine the logic, then transform to TypeScript"
Critic + Refine:
"Review this code, identify issues, then fix each one iteratively"
Questions & Answers
Key Takeaways
- Scaffold: structure first, then fill — prevents overwhelm and ensures coherent architecture
- Critic: generate then review — catches issues that generation misses
- Refine: iterate along one dimension at a time — shapes output toward quality
- Transform: convert between forms while preserving semantics — reliable and predictable
- Build a library: save prompts that work well and reuse them systematically
- Combine patterns: the most effective workflows chain multiple patterns together
Next Steps: In Lesson 10 — Meta-Prompting & Self-Improvement, you'll learn how to use AI to improve your prompts themselves — and build an ever-improving feedback loop.