Refactoring and documenting code with AI
Learn a safe routine for cleaning up AI-written code: write tests first, refactor in small steps, review the diff, and let AI draft comments and a README that you then verify.
Refactoring means changing how code is written without changing what it does. AI is good at it: it can rename things, split long functions and remove repetition quickly. It is also risky, because it may quietly change behaviour while it 'tidies'.
In this tutorial you will refactor a small piece of the Brew & Bean cart using a safe routine: write tests first, refactor in small steps, run the tests after every step. You will also let AI draft comments and a README, and learn to check them.
Prerequisites: Node.js installed and a Vite project (see *Start the React project with Vite*), Git, and an AI assistant (see *AI coding tools compared*). Basic JavaScript is enough.
Why refactor at all
AI-written code often works but is messy: vague names, copy-pasted blocks, functions doing three jobs. Messy code is slow to change and hides bugs. You will read code many more times than you write it, so readability is worth a few minutes.
Refactor when you are about to add a feature to messy code, when you cannot explain a function in one sentence, or when you see the same block three times. Do not refactor just because the code is 'not how I would write it'.
The starting code
This function should calculate an order total from the cart items and the menu. It works, but nobody can tell what f, a, c or t mean.
// src/cart.js (the "before": it works, but it is hard to read)
export function f(a, c) {
let t = 0;
for (let i = 0; i < a.length; i++) {
for (let j = 0; j < c.length; j++) {
if (a[i].item_id == c[j].id) {
t = t + a[i].qty * c[j].price;
}
}
}
if (t > 500) {
t = t - t * 0.1;
}
return t;
}
Each cart item is {item_id, qty}; each menu item is {id, price}. Orders above 500 rupees get 10% off.
Step 1: lock the behaviour with tests
A refactor is only safe if you can prove nothing changed. So first write tests for the current behaviour. Install Vitest, a test runner that works well with Vite.
npm install -D vitest// src/cart.test.js
import { describe, it, expect } from "vitest";
import { f } from "./cart.js";
const menu = [
{ id: "espresso", price: 120 },
{ id: "cappuccino", price: 180 },
{ id: "croissant", price: 150 },
];
describe("order total", () => {
it("adds up price x quantity", () => {
expect(f([{ item_id: "espresso", qty: 2 }], menu)).toBe(240);
});
it("returns 0 for an empty cart", () => {
expect(f([], menu)).toBe(0);
});
it("ignores items that are not on the menu", () => {
expect(f([{ item_id: "pizza", qty: 1 }], menu)).toBe(0);
});
it("gives 10% off when the total is above 500", () => {
// 3 x 180 = 540, minus 10% = 486
expect(f([{ item_id: "cappuccino", qty: 3 }], menu)).toBe(486);
});
it("gives no discount just below 500", () => {
// 1 x 180 + 2 x 150 = 480
const cart = [
{ item_id: "cappuccino", qty: 1 },
{ item_id: "croissant", qty: 2 },
];
expect(f(cart, menu)).toBe(480);
});
});
The last test checks a total just under the discount threshold. Boundary cases like this are where refactors often break things.
Add a test script to package.json under "scripts": "test": "vitest run". Then run it.
npm testTest Files 1 passed (1)
Tests 5 passed (5)You can ask AI to propose the tests, but you must read them. A test that expects the wrong number will happily pass on wrong code.
Here is a JavaScript function. I want to refactor it, so first I need tests that describe its CURRENT behaviour exactly, including edge cases. Do not change the function.
```
[paste the function]
```
Write tests using Vitest. Include: a normal case, an empty input, a boundary case, and one case with bad input (for example an unknown item). For each test, add a one-line comment with the expected value worked out by hand.What to expect: Check the hand-worked numbers yourself. If the function has a bug, the test should record today's behaviour and you note the bug separately. Don't fix bugs and refactor in the same step.
Step 2: ask for a plan, not just a rewrite
Refactor the function below for readability. Rules:
- Behaviour must stay exactly the same; my tests must still pass.
- Rename variables and the function so they say what they mean.
- Replace magic numbers with named constants.
- Remove the nested loop if there is a simpler way.
- Do not add libraries or new features.
First list the changes you plan to make as bullets. Then show the refactored code. Then tell me anything whose behaviour might differ.
```
[paste the function]
```What to expect: The last line is the important one. A good answer may mention, for example, that == became a Map lookup, or that an unknown item now adds 0.
Asking for the plan first lets you spot a bad idea before it becomes code. Asking 'what might differ' makes the model check its own work.
Step 3: apply and run the tests
// src/cart.js (the "after": same behaviour, clearer)
const DISCOUNT_THRESHOLD = 500; // rupees
const DISCOUNT_RATE = 0.1;
export function calculateOrderTotal(cartItems, menu) {
const priceById = new Map(menu.map((item) => [item.id, item.price]));
const subtotal = cartItems.reduce((sum, { item_id, qty }) => {
const price = priceById.get(item_id) ?? 0;
return sum + price * qty;
}, 0);
return subtotal > DISCOUNT_THRESHOLD
? subtotal * (1 - DISCOUNT_RATE)
: subtotal;
}
Map gives a fast price lookup by id, so the nested loop is gone. reduce adds up price x quantity. Named constants replace the numbers 500 and 0.1.
The tests still call f, so rename the import and the calls to calculateOrderTotal in src/cart.test.js (a find-and-replace in your editor), then run npm test.
Test Files 1 passed (1)
Tests 5 passed (5)Floating point surprises
Multiplying money by 0.9 can give numbers like 486.00000000000006 in JavaScript. Our test values happen to come out exact. For real shops, work in whole rupees or paise and round at the end. The server computes the real total anyway, as in *Build the API with FastAPI*; the browser total is only a preview.
If a test fails after the refactor
Do not edit the test to make it pass. Either the refactor changed behaviour (fix it, or undo it) or the old behaviour was a bug (decide that on purpose, then change the test knowingly). Undo an uncommitted change with Git: git restore src/cart.js.
Small steps, with Git
Commit before you start and after each green test run. If an AI change goes wrong you can go back in seconds.
The safe refactor routine
- 1Commit your working code.
- 2Write or generate tests for the current behaviour, read them, and make them pass.
- 3Ask AI for a plan, then the refactored code for ONE function.
- 4Paste it in and run the tests.
- 5If green, read the diff and commit; if red, undo and adjust the prompt.
git add -A
git commit -m "Add tests for order total"
# ... refactor ...
npm test
git add -A
git commit -m "Refactor order total for readability"
# see exactly what the last commit changed:
git show HEADgit diff (before committing) or git show HEAD (after) lists every changed line. Read it before you accept AI changes.
Never ask AI to 'refactor the whole project' in one go. It will touch many files and you cannot review that. Pick one function or one file per request.
Documenting code with AI
AI writes decent comments and READMEs, because it can read the code and describe it. The danger is comments that describe what the AI assumes the code does. A wrong comment is worse than no comment.
Good comments explain why, not what. // add 1 to i is noise. // orders above 500 get 10% off (owner's rule) is useful. Doc comments that describe inputs and outputs are useful too:
/**
* Calculates the total for an order in rupees.
*
* Orders above 500 get a 10% discount. Items that are not on the
* menu are ignored (they add 0 to the total).
*
* @param {{item_id: string, qty: number}[]} cartItems
* @param {{id: string, price: number}[]} menu
* @returns {number} the total in rupees
*/
JSDoc is the standard comment format for JavaScript. Editors like VS Code show it when you hover over the function.
Write a JSDoc comment for this function. Describe what it does, each parameter with its type, and the return value. Mention edge cases only if the code really handles them. Do not guess about behaviour you cannot see in the code; if something is unclear, list it as a question for me instead.
```
[paste the function]
```What to expect: Check each sentence against the code. Does 'ignored' really happen for unknown items? Here it does, because of ?? 0.
Write a README.md for this project. I will paste the file list and package.json. Include: what the project is, how to install and run it (exact commands from package.json), how to run tests, and the folder structure in one line per folder. Do not invent features or commands that are not in what I pasted.
File list:
[paste your file listing]
package.json:
[paste package.json]What to expect: Run every command in the README on a clean copy of the project. If any fails, the README is wrong.
How this comes up in interviews
Interviewers ask 'How do you make sure a refactor doesn't break things?' A strong answer: tests first, small steps, run tests after each, review the diff, commit often. They may also give you ugly code and ask you to clean it up while you talk through it. Practise naming things clearly and explaining why each change helps.
Try it: Refactor the order form validation
Pick a function from your own coffee shop project, for example the one that validates the order form (name, phone, at least one item). 1) Ask AI for tests of its current behaviour and read them. 2) Run the tests and commit. 3) Ask for a refactor with a plan first. 4) Run the tests again and read the diff. 5) Ask AI for a JSDoc comment and check each sentence.
Try it: Catch a hidden change
Restore the original function f (use Git). Ask AI to 'simplify' it with no rules and no plan, paste the result in, and run your 5 tests. Does anything fail? Then try again with the full refactor prompt from this lesson.
Key takeaways
- Refactoring changes structure, not behaviour. Prove that with tests.
- Write tests first, refactor in small steps, and run the tests after each step.
- Ask AI for a plan and for 'what might behave differently' before it shows code.
- Commit before and after, and read the diff to see exactly what changed.
- Never edit a test just to make it pass after a refactor.
- Comments should explain why. Check every AI-written comment and README against the real code and commands.
Check yourself
Finishing the quiz marks the lesson as done.
1. What must you have in place before asking AI to refactor a function?
2. After a refactor, one test fails. What is the right first move?
3. Why ask the AI to list its planned changes before showing code?
4. Which is the most useful kind of code comment?
5. Which request is best for AI-assisted refactoring?
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.