Working with AI Effectively
Advanced · 17 min read · ▶ live playground · ✦ checkpoint
AI gets much more useful when you stop treating prompts like magic spells and start treating them like work orders. A good prompt puts the goal, the relevant context, the constraints, the desired output, and the checks in one place so the assistant has a real task instead of a vague wish.
In the last lesson, you learned that AI output is a draft. This lesson is about getting better drafts, improving them through iteration, and reviewing generated JavaScript with the same seriousness you would bring to a teammate's pull request.
The prompt is a work order
A prompt is the work order clipped to a task. If the work order says "fix my app," the assistant has to guess the app, the bug, the rules, and what "fixed" means. If the work order says "this function should reject invalid emails, here is the failing test, keep the public API the same, and return a minimal patch," the assistant has something concrete to do.
Good prompts usually include six ingredients:
- Goal: what outcome you want.
- Context: the files, snippets, errors, logs, tests, or behavior that matter.
- Constraints: the guardrails the answer must stay inside.
- Project rules: style, framework, runtime, package limits, naming, and existing patterns.
- Desired output: explanation, patch, test plan, alternatives, checklist, or a small snippet.
- Checks to run: lint, typecheck, unit tests, end-to-end tests, build, or a specific command.
Context is the desk surface. If the desk only has one stack trace on it, the assistant will build its answer around that. If the desk has the relevant function, failing test, expected behavior, package version, and command output, the answer can be much more specific.
Compare these two prompts:
Fix this validation bug.That prompt is short, but most of the job is missing. What kind of validation? What code? What should happen? What must not change?
Goal: Fix the email validation in validateSignupInput.
Context:
- validateSignupInput currently accepts "ada@example" as valid.
- The UI should show "Enter a valid email address" for that value.
- Existing callers expect the function to return { ok: boolean, errors: string[] }.
- The relevant file is src/signup/validateSignupInput.js.
Constraints:
- Keep the return shape unchanged.
- Do not add a new dependency.
- Use the same error-message style already in the file.
Output:
- Suggest the smallest code change.
- Add or update unit tests for the failing case.
- List the checks I should run afterward.This is not about writing more words for their own sake. It is about removing guesswork. The assistant may still be wrong, but now it is wrong against a clearer target.
Put real evidence on the desk
Beginners often ask AI about a bug using only their conclusion: "the save button is broken." That skips the evidence a developer needs.
Bring the actual material:
- The smallest relevant file or function.
- The exact error message, including the stack trace line if there is one.
- What you did before the bug happened.
- What you expected.
- What happened instead.
- The command that failed, plus the important output.
- The runtime: browser, Node, server route, worker, test environment, or build step.
That last one matters in JavaScript. document exists in a browser page, but not in Node. process.env exists in Node, but not in ordinary browser code. A fetch call may work differently in a server route than it does in a client component. If you do not name the runtime, the assistant may choose the wrong world.
Project rules are context too. If your codebase uses const first, explicit semicolons, textContent for DOM text, small pure helpers, and tests near the code, say that. If the project forbids new dependencies, say that. If a function is public API and cannot change its return shape, say that loudly.
Constraints are guardrails. They do not guarantee a safe answer, but they reduce the space of bad ones.
Iterate instead of accepting
The first answer is rarely the best answer. Treat it like a first draft from a teammate who typed quickly.
Ask for alternatives when the decision is not obvious:
Give me two approaches: one minimal patch and one cleaner refactor.
For each, explain the tradeoff and the risk.Ask for a narrower scope when the assistant tries to redesign too much:
Keep this to the smallest behavior-preserving change.
Do not rename files, change public function names, or introduce a new package.Ask for tests when the answer changes behavior:
Add tests that fail before the change and pass after it.
Include the edge cases this code is most likely to miss.Ask it to compare options, not just produce one:
Compare these two versions for readability, mutation risk, async behavior, and browser compatibility.
Pick one only after listing the tradeoffs.Iteration is not arguing with the assistant until it says what you want. It is using questions to reveal assumptions. "Why this approach?" "What edge case does it miss?" "What would break if this input is null?" "Can this run in a worker?" "What test would catch a regression?"
The best prompts make the assistant show its work in useful ways. You do not need a long essay every time. Sometimes the right output is a tiny patch plus three bullets: what changed, why, and what checks to run.
Review generated code like a pull request
Generated code is a borrowed tool with a covered blade. You do not grab it by the sharp end just because it arrived quickly.
Start with the diff. Read every changed line. If the assistant changed files you did not ask it to touch, find out why. If it rewrote a working function to make a tiny fix, slow down. A smaller, clearer change is often safer than a dramatic one.
Then trace the behavior. Follow the input through the code to the output. In a UI, ask what the user sees before, during, and after the action. In a Node script, ask what happens when a file is missing. In an API handler, ask what status code and JSON body come back for success and failure. In async code, ask what happens if the promise rejects, resolves late, or runs twice.
Run the ordinary checks. For many JavaScript projects that means lint, typecheck if the project has TypeScript, unit tests, integration or end-to-end tests when relevant, and the build. If there is a specific bug, add or run a test that proves that bug is fixed. A green build does not prove the product is right, but a red build is useful evidence.
Check the runtime boundary. Is this browser-only code importing a Node built-in? Is server code reaching for document? Is a worker assuming access to the DOM? Is generated code using an API that your target browser or installed package version does not support?
Check the risky surfaces. User input, DOM insertion, auth checks, secrets, environment variables, dependency changes, database queries, and network calls deserve extra attention. If the code touches money, personal data, permissions, or security, the assistant's confidence matters less than your verification.
Finally, check maintainability. Does the code fit the style around it? Are names clear? Did it duplicate logic that already exists? Did it hide a simple idea behind a clever abstraction? Could the next developer understand it without reading your prompt?
Make the code yours
AI can speed up typing, searching, explaining, and drafting. It cannot absorb the responsibility for your code. Before you keep generated code, make sure you can answer three questions:
- What problem does this solve?
- Why is this approach acceptable here?
- What evidence says it works?
If you cannot answer those yet, keep iterating. Ask for an explanation. Ask for a smaller change. Ask for the test that proves the behavior. Then read and run the result yourself.
Next, you will tighten the safety side of this workflow: secrets, privacy, licenses, hallucinated APIs, and the learning risk of letting the assistant do too much of your thinking.
Checkpoint
Answer all three to mark this lesson complete