Have you ever pasted a work item into a GenAI tool, hit enter, and gotten back something that completely missed the point? I have. More than once. And each time, the problem wasn’t the model. It was my prompt.

Over the past year or so, I’ve been refining how I interact with GenAI for coding tasks. What I’ve settled on is a process rooted in Test-Driven Development (TDD) that gives me a repeatable way to get useful output. Here’s how it works.

Start by Asking How to Ask

Before calling an unfamiliar API, I’d read its docs first. I treat GenAI the same way. Before writing my actual prompt, I ask the model how I should prompt it for the task at hand.

In practice, I’ll paste in the requirements from a work item and write something like:

“Here are the details for the work item: [paste details]. I want to generate TypeScript unit tests that satisfy these requirements. What’s the best way to prompt you for that?”

I think of this as a “meta-prompt.” The model comes back with specifics about what structure works best, which details matter most, and how to frame the request. That response becomes my starting template for the actual task.

I’ve started saving the good ones as reusable snippets. When a teammate is working on a similar problem, I can share the template instead of having them start from scratch.

Set the Stage Before You Prompt

Before I run the red-green-refactor loop, I spend a minute setting up context. In my experience, this is the difference between code that almost works and code that fits into the codebase.

Assign a role. I tell the model who it is: “You are a senior TypeScript developer working in a Node.js microservices codebase that uses Express and Prisma.” This steers the model toward the right idioms, patterns, and conventions. Without it, I’ve gotten perfectly valid code that didn’t match our stack at all.

Provide codebase context. I paste in the relevant types, interfaces, or the file I’m working in. For example, if I’m adding a discount function, I’ll include the existing Order type and the CustomerTier enum so the model writes code that fits our data model instead of inventing its own. The more specific the context, the less I have to fix later.

Show an example when describing isn’t enough. Sometimes the meta-prompt gives me a template, but the model still doesn’t produce the style of test or code I want. When that happens, I paste in one concrete example: “Here’s what a test in our codebase looks like” followed by an actual test from the repo. This is what’s called few-shot prompting, and it’s often more effective than a paragraph of instructions.

Use structure for longer prompts. For simple one-liners, plain English is fine. But when my prompt has multiple pieces (role, context, instructions, examples), I separate them with Markdown headers or XML-style tags:

## Role
You are a senior TypeScript developer...

## Context
<existing_code>
// paste the relevant file or types here
</existing_code>

## Task
Write a Jest test for...

## Output Format
Return only the test file. No explanation.

This kind of structure helps the model parse what’s an instruction versus what’s reference material. I started doing this after noticing that the model would sometimes treat my pasted code as part of the instructions rather than context.

Specify the output format. I’ve learned to be explicit about what I want back. “Return only the function, no explanation” or “Return the full updated file” saves a round trip. Without this, I’d often get a chatty explanation wrapped around the code, and I’d have to ask again for just the code.

Use the Red-Green-Refactor Loop

Here’s where TDD comes in. Instead of asking GenAI for a feature and its tests all at once, I run the same red-green-refactor cycle I’d use in my own development workflow.

Red (write a failing test first). I ask the model to write a test that defines the behavior I need. For example: “Write a Jest test for a function calculateDiscount that returns a 10% discount for orders over $100.” Starting with the test forces me to be clear about what I actually want before any implementation exists.

Green (write the minimum code to pass). Next, I feed that test back and ask for the implementation: “Here is a Jest test. Write the calculateDiscount function to make this test pass.” Keeping both steps in the same conversation helps the model retain context.

Refactor (add the next requirement and repeat). Requirements evolve, so I keep going. “Add a new Jest test so VIP customers receive a 20% discount regardless of order total. Give me the full, updated test file.” Then: “Update the implementation to pass all tests. Preserve existing behavior for non-VIP customers.”

I keep cycling through red-green-refactor for edge cases like negative totals, rounding, and discount caps. Each pass adds a test first, then the code to satisfy it. The growing test suite catches regressions as the implementation changes.

In my experience, this approach works because it keeps the conversation focused. Each prompt has a single, verifiable goal. The model doesn’t have to guess what “build me a discount feature” means because the test already defines it.

Refactor the Output

Once the tests pass, I’m not done. I treat the generated code the same way I’d treat a pull request from a junior developer: I review it and ask for specific improvements.

The key word there is “specific.” Asking GenAI to “make this better” gets vague results. Here’s what I do instead. I ask for one thing at a time:

  • “Refactor for readability. Extract well-named helpers and reduce nesting without changing behavior.”
  • “Check this against SOLID principles. Point out any violations and propose minimal refactors.”
  • “List likely edge cases I haven’t covered. Update the code to handle them.”
  • “Do a quick security review for injection, path traversal, and secrets handling.”

I’ve found that short, focused review prompts get better results than long checklists. When I dump ten requests into one prompt, the model tends to give shallow answers across all of them. One at a time goes deeper.

Over the course of several projects, the review prompts I come back to most often are readability, SOLID adherence, and edge case coverage. Your list will probably look different depending on what you tend to miss in your own code reviews.

When the Loop Breaks Down

This process is not foolproof. Sometimes the model generates a bad test, or the code doesn’t pass, or the conversation gets too long and confused. When that happens, I go back to the red-green-refactor mindset.

If the test is wrong, I correct it directly: “This test ignores negative values. Regenerate it and include a negative input case.”

If the code fails, I paste the error alongside the failing test: “This fails with [error]. Fix the implementation.”

If the conversation has gotten too tangled to recover, I reset: “Let’s start fresh on this function. The most important requirement is [state it clearly].”

A note on why conversations get tangled in the first place: models have a finite context window. Every prompt and response in a conversation accumulates. After several red-green-refactor cycles with large code blocks, the earlier context starts getting pushed out or diluted. In my experience, resetting after four or five substantial exchanges tends to keep things sharp. It’s not a failure of the process; it’s just how context windows work.

Adjust for the Model You’re Using

One thing I’ve had to learn the hard way is that different models — and different modes of the same model — respond better to different prompting styles. When a model is reasoning through a problem (extended thinking or dedicated reasoning modes), it tends to do well with high-level goals. You can say “implement this feature given these tests” and let the model work out the steps. When a model is running in its standard, fast mode, it benefits from more explicit, step-by-step instructions.

The line used to fall between model families, but most frontier models today are hybrids that do both — some with an explicit thinking toggle, some that route automatically based on the task. So the practical question isn’t “which model am I using?” It’s “is the model going to think through this problem, or execute my instructions?” If it’s thinking, keep the prompt short and goal-oriented. If it’s executing, give it the full structure I described earlier: role, context, explicit task, output format.

In practice, this means my prompts for reasoning models are shorter and more goal-oriented, while my prompts for standard models include more of the structure I described earlier (role, context, explicit task, output format). If you’re not sure which style your model prefers, start with the more structured approach. It rarely hurts, and you can always trim back.

Next Step

Pick a coding task you’re working on this week and try this approach. Set up your context (role, relevant code, output format), write the test prompt first, get the implementation second, then refactor with specific review prompts. Pay attention to where the loop breaks down for you and what you have to correct. Those corrections are your best signal for writing better prompts next time.

I’m Peter

I’ve spent my career building software and leading engineering teams. I started as a developer and architect, grew into engineering leadership, and today I serve as a Chief Technology Officer.

Here, I share practical insights on technology, leadership, and building high-performing teams.

Connect with me on LinkedIn.

Discover more from Peter Mourfield

Subscribe now to keep reading and get access to the full archive.

Continue reading