Prompt Engineering for Coding: A Practical Guide

# Prompt Engineering for Coding: A Practical Guide

## Introduction

If you’re still typing “write me a function” into ChatGPT and wondering why the output is garbage, this guide is for you. Prompt engineering isn’t magic—it’s a skill. And like any skill, it has patterns that work.

I’ve spent the last two years using AI assistants daily in my workflow—debugging production issues, generating boilerplate, refactoring legacy code. The difference between a prompt that wastes your time and one that delivers usable code often comes down to four or five specific techniques.

This isn’t about “prompt hacks” or viral tricks. It’s about how to communicate with an LLM so it actually understands what you need. We’ll look at real prompts, real outputs, and the adjustments that make them work.

## Why Prompt Engineering Actually Matters

The model doesn’t know your codebase, your constraints, or your intent. It knows patterns in training data. Your job is to collapse the gap between “what you want” and “what it infers.”

A vague prompt produces vague code. An imprecise prompt produces code that almost works but fails on edge cases. The time you save by writing a better prompt often exceeds the time you’d spend debugging the slop it would otherwise produce.

This isn’t about being polite to the AI. It’s about being precise for yourself.

## The Anatomy of an Effective Coding Prompt

Every effective coding prompt contains five elements:

1. **Context** — What does the code need to integrate with?
2. **Task** — What exactly should it do?
3. **Constraints** — What should it avoid or handle?
4. **Format** — How should the output be structured?
5. **Verification** — How will you know it works?

Most prompts fail because they only state the task. Let’s build one from scratch.

## Technique 1: Context Framing

Don’t start with what you want. Start with where it lives.

**Weak prompt:**
“`
Write a Python function to parse dates.
“`

**Strong prompt:**
“`
We have a Django REST API returning dates as ISO strings in the ‘created_at’ field of a serializer. Write a Python function that parses these dates and returns them in ‘Month Day, Year’ format for display in our admin panel.
“`

The strong version tells the model:
– The framework (Django REST)
– The data format (ISO strings)
– The destination (admin panel display)
– The output format (Month Day, Year)

This context prevents the model from generating something that works in isolation but breaks in your actual code.

## Technique 2: Constraint Specification

Constraints are where most developers save themselves debugging time. Specify what the code should handle explicitly.

**Weak prompt:**
“`
Write a function that validates user input.
“`

**Strong prompt:**
“`
Write a Python function that validates user registration input with these constraints:
– Email must be a valid format (we use django.core.validators)
– Password must be at least 12 characters
– Username must be alphanumeric, 3-30 characters
– Return a dict with field names as keys and error messages as values
– Do not include password strength estimation (handled elsewhere)
“`

The constraints prevent the model from over-generating (adding features you don’t want) or under-generating (missing edge cases you’ll hit at 2am).

## Technique 3: Step-by-Step Decomposition

For complex tasks, decompose. The model has a context window limit, and monolithic prompts lose fidelity mid-output.

**Weak prompt:**
“`
Build a full authentication system with JWT, refresh tokens, and user roles.
“`

**Strong prompt (decomposed):**
“`
First task: Write a Pydantic model for our user schema with email, password, and role fields.

Second task: Write a JWT token generator function that accepts a User object and returns access_token and refresh_token.

Third task: Write a refresh token endpoint that validates the refresh token and issues a new access token.

Use these imports:
from pydantic import BaseModel, EmailStr
import jwt
from datetime import datetime, timedelta
“`

This approach gives you three separate, verifiable outputs instead of one massive block that you’ll struggle to validate.

## Technique 4: Output Scaffolding

Tell the model exactly how to structure its response. This makes review faster and debugging easier.

**Prompt with scaffolding:**
“`
Write a React component that displays a paginated table. Output should follow this structure:

1. TypeScript interfaces (at the top)
2. Component with props interface
3. Helper functions after component
4. Default export

Include JSDoc comments on each function explaining:
– Parameters
– Return type
– Any side effects
“`

This prevents the common problem where the model buries the interface definitions in the middle of the file, or omits documentation entirely.

## Technique 5: Test-Aware Prompting

Ask the model to write tests alongside the code—or even before it. This forces it to think about edge cases.

**Prompt:**
“`
Write a TypeScript function that calculates cart totals with tax and shipping. Then write three Jest test cases covering:
– Normal case with items, tax, and shipping
– Empty cart edge case
– Decimal precision edge case (our currency is USD, 2 decimal places)
“`

When the model writes tests first, it reveals assumptions you might have missed. Then you get both the implementation and the verification logic in one go.

## Common Mistakes to Avoid

**Asking too much at once.** A prompt asking for a full CRUD API with authentication, error handling, and tests will produce shallow code in all areas. Split it up.

**Forgetting the environment.** If you need code for a specific version of a library, say so. “Using FastAPI 0.104 and Pydantic v2” is a complete sentence that prevents version mismatch bugs.

**Assuming the model knows your conventions.** It doesn’t know you use snake_case, or that your error handling follows a specific pattern. Tell it: “Use our project’s error handling pattern: wrap in try/catch and raise CustomException.”

**Not specifying what you don’t want.** If you don’t want it to use a specific library or approach, say so explicitly. “Do not use external libraries beyond lodash” prevents unwanted dependencies.

## Key Takeaways

– Vague prompts produce vague code. Precision is not overhead—it’s debugging saved.
– Always include context: framework, data formats, integration points.
– Specify constraints explicitly, including what to exclude.
– Decompose complex tasks into sequential prompts for better results.
– Scaffold your output format to match your team’s code conventions.
– Ask for tests alongside code to surface edge cases early.

## Next Steps

1. Take one prompt from your recent history that produced bad output. Rewrite it using the five elements framework (context, task, constraints, format, verification).

2. Try decomposing a multi-step task into three separate prompts. Compare the output quality to a single monolithic prompt.

3. Pick a function in your current codebase that you hate writing tests for. Use a test-aware prompt and see what the model produces.

Prompt engineering is a skill that compounds. The better you get at communicating intent, the more time you’ll save. Start with your next prompt.