Prompting Like an Engineer
The difference between a frustrating AI session and a productive one is rarely the model. It is the prompt. Most beginners treat the AI like a search engine — they type a vague wish and hope for the best. Senior engineers treat it like a sharp but literal junior teammate: someone fast and capable, but who will build exactly what you describe, including your mistakes.
This chapter is about closing that gap. You do not need to memorize tricks. You need to communicate the way a good engineer communicates a task: with context, constraints, and a clear definition of done.
Give Context Before You Give the Task
The AI cannot see your project the way you can. It does not know your framework version, your file structure, or the conventions you have already established. When you skip this, it guesses — and guesses badly.
Compare these two prompts for the same request.
Add a function to validate emails.
This is a TypeScript backend using Express and Zod for validation.
We already validate inputs with Zod schemas in src/schemas/.
Add an email validation schema in that style. Emails must be
lowercase, max 254 chars, and reject disposable domains from
the existing BLOCKED_DOMAINS list in src/config.ts.
The first prompt produces generic code that may use a regex you do not want, in a style that does not match your codebase. The second produces code you can paste in. The rule: state the stack, the conventions, and the existing pieces the AI should reuse.
Specify Inputs, Outputs, and Edge Cases
A task is not defined until its boundaries are defined. Senior engineers think in terms of "what goes in, what comes out, and what happens when things go wrong." Bake that into the prompt.
Write a function to parse a date string.
Write a function parseDate(input: string): Date | null.
Inputs: ISO 8601 strings ("2026-06-14", "2026-06-14T10:30:00Z").
Output: a Date object, or null if the string is invalid.
Edge cases to handle:
- empty string or whitespace -> null
- valid format but impossible date (2026-02-30) -> null
- trailing garbage ("2026-06-14xyz") -> null
Do not throw. Return null for all invalid input.
When you name the edge cases, the AI handles them. When you do not, it writes the happy path and you discover the gaps in production. If you are not sure what the edge cases are, ask the AI first: "What edge cases should a date parser handle?" — then feed the answer back as constraints.
Work in Small Steps
The single most common mistake is asking for too much at once. "Build me a user dashboard with auth, charts, and a settings page" will produce a wall of code you cannot review, with bugs spread across files you did not read.
Small steps keep you in control:
- Ship one function, component, or endpoint per prompt.
- Verify each piece works before moving on.
- Build on confirmed-working code, not a tower of unverified output.
Build the whole authentication system with login, signup,
password reset, sessions, and email verification.
Step 1 of the auth flow: write just the signup endpoint.
POST /signup taking { email, password }. Hash the password
with bcrypt, store the user, return 201 with the user id.
Assume the User model and db client already exist (I'll show
you their shapes). We'll handle login next.
Smaller prompts mean smaller diffs, and smaller diffs mean you can actually read what changed. That is the whole game.
Ask for a Plan Before Code
For anything non-trivial, separate thinking from typing. Ask the AI to outline its approach first. This catches wrong assumptions before they become 200 lines of wrong code, and it is far cheaper to fix a plan than a pull request.
I want to add rate limiting to my API. Before writing any code,
give me a short plan: where the limiter should live, what
storage it needs, what happens when a limit is hit, and any
tradeoffs. Don't write code yet — I'll approve the approach first.
When the plan looks right, say "good, implement step 1." When it does not, you have spent thirty seconds correcting course instead of an hour untangling code. Planning first is the habit that most separates engineer-style prompting from wishful prompting.
Iterate on Diffs, Not Rewrites
Once code exists, resist the urge to ask for the whole thing again. Vague follow-ups like "make it better" throw away working code and reintroduce bugs you already fixed. Point at the specific change you want.
That's not right, rewrite the whole thing.
The function works, but two issues:
1. The error on line with `throw new Error` should return a
Result type instead of throwing — match the pattern in
the validateUser function above.
2. The loop re-reads `items.length` each iteration; hoist it.
Show me just the diff for those two changes.
Asking for a diff (rather than a full rewrite) keeps changes reviewable and preserves the parts that already work. Treat the AI's output like a colleague's pull request: comment on specific lines, request targeted edits.
Provide Examples
One concrete example is worth a paragraph of description. If you want output in a particular shape, show the shape. This is the fastest way to eliminate ambiguity.
Format the data nicely.
Transform this array of users into a summary object.
Input:
[{ name: "Ann", role: "admin" }, { name: "Bo", role: "user" }]
Desired output:
{ total: 2, byRole: { admin: ["Ann"], user: ["Bo"] } }
Write the transform function and a test matching this example.
Examples pin down naming, structure, and casing that words alone leave fuzzy. When you can, include the example as a test — then "make the test pass" becomes an unambiguous instruction.
Common Anti-Patterns to Avoid
These are the prompting habits that quietly waste your time. Watch for them in your own sessions.
- The vague wish. "Make a good login page." Good by whose standard? Replace adjectives with specifics: which fields, which validation, which framework.
- The mega-prompt. Dumping an entire feature in one message. Break it into steps you can verify one at a time.
- The silent context. Assuming the AI remembers or knows your project. Restate the relevant files, types, and conventions — especially in a fresh conversation.
- The blind accept. Pasting generated code without reading it. The AI is confident even when wrong; you are still the engineer of record.
- The endless retry. Re-running "try again" hoping for different output. If two attempts fail, the prompt is the problem — add context or constraints, do not just reroll.
- The moving target. Changing requirements mid-thread without restating them. Summarize the current spec when you pivot, so the AI is not working from a stale picture.
The Core Mindset
Every technique here reduces to one idea: say what you would have to say to a competent teammate who cannot read your mind. Context up front. Clear inputs and outputs. Small, verifiable steps. A plan before the code. Targeted edits over rewrites. Examples where words fall short.
You are not writing magic incantations. You are writing a spec, briefly and precisely, and then reviewing the result. Do that consistently and the AI stops feeling like a slot machine and starts feeling like the fast, tireless engineer it can actually be.