Refactoring JavaScript with Claude Code demands more discipline than refactoring by hand or with a chat-based assistant. Claude Code operates as an agentic CLI tool, meaning it can read files, write changes, create new modules, and execute shell commands autonomously within a single session. That power makes a structured workflow essential rather than optional. Without one, you risk scattered edits across files, broken imports, wasted context tokens, and regressions that only surface after the session ends. This article walks through a concrete, repeatable pattern for using Claude Code to decompose a monolithic Express.js route handler into clean, separated modules, step by step, with full prompts and code.
How to Refactor Code with Claude Code
- Configure a
CLAUDE.mdfile declaring in-scope and read-only files, session rules, and project context. - Branch your repository with
git checkout -b refactor/to create a clean rollback point. - Prompt Claude Code to read all target files and map every dependency, explicitly forbidding modifications.
- Verify no files were changed during analysis by running
git diff --name-only. - Request a step-by-step refactoring plan including exact file changes, new exports, and import rewiring.
- Review and approve (or adjust) the proposed plan before any execution begins.
- Execute changes one step at a time, committing after each step and running
npm testto catch regressions. - Sweep all changed files for orphaned imports, unused variables, and dead code before submitting your PR.
Table of Contents
Why Refactoring with Claude Code Is Different
Chat-based AI assistants produce code snippets that developers paste by hand into their editors. Claude Code does something fundamentally different: it operates directly on the filesystem. It reads source files, edits them in place, creates new files, runs test suites, and chains these actions together. You can configure its default behavior and approval granularity — check Anthropic’s Claude Code documentation for the --approval or equivalent flags for your installed version. This agentic behavior makes it powerful for refactoring. It also makes undisciplined use dangerous, because the tool will happily chain together destructive changes before you understand what happened.
This agentic behavior makes it powerful for refactoring. It also makes undisciplined use dangerous, because the tool will happily chain together destructive changes before you understand what happened.
When a developer types “refactor this file” without constraints, an unconstrained prompt can cause Claude Code to rewrite the target file, update imports in dependent files, and rename exports in a single action, all before the developer has reviewed the dependency graph. In manual refactoring, the developer naturally builds a mental model of the code before touching anything. Without explicit instruction, Claude Code skips that step. A structured workflow restores that deliberate sequencing, ensuring that reading always precedes writing.
The Read-Before-Changing Pattern Explained
What Is the Read-Before-Changing Pattern?
Never allow Claude Code to modify any file until it has first mapped the complete dependency graph of the refactoring target. This means reading all relevant files, identifying every import, export, shared constant, side effect, and cross-file reference before a single line changes.
The common anti-pattern is prompting with something like “Extract the validation logic from orders.js into a separate module.” That prompt gives permission to start editing immediately. Without a full read pass, the tool can miss a shared variable referenced three files away. The read-before-changing pattern treats analysis and modification as strictly separate phases, which aligns with how Claude Code manages context: the more complete its understanding of the codebase before acting, the more consistent its changes.
The Three Phases at a Glance
The workflow divides into three distinct phases. During Phase 1, Claude Code analyzes the target files and produces a dependency report without editing anything. In Phase 2, the tool proposes a step-by-step refactoring plan that the developer reviews, adjusts, and approves. Execution and verification come last: Phase 3 applies changes incrementally, with test runs and git commits between each step. The sections below walk through each phase using a real JavaScript refactoring example.
Setting Up Your Refactoring Session
Prerequisites
Before starting, confirm the following:
- Node.js 20.x is installed (
node --versionshould returnv20.x.x). - Claude Code CLI is installed and authenticated. Run
claude --versionto confirm. Shell execution permissions must be enabled via theallowedToolsconfiguration in your Claude Code settings file (typically~/.claude/settings.json) for Claude Code to run commands likenpm teston your behalf. - Git is initialized in your project root.
- Your project has a
package.jsonwith Express and Jest as dependencies. Runnpm list express jestto verify versions. The example below assumes Express 4.18.x and Jest 29.x; Express 5.x introduced breaking changes to async error handling, so verify your version matches.
Project Structure and CLAUDE.md Configuration
Claude Code reads a CLAUDE.md file at the project root (the filename is case-sensitive; use uppercase CLAUDE.md on Linux and macOS) to pick up session-level instructions. Verify your Claude Code version supports this feature. For refactoring work, this file should declare which files are in scope, which are off-limits, and what rules Claude Code must follow during analysis.
# CLAUDE.md — Refactoring Session Configuration
## Scope
- Files in `src/routes/` and `src/utils/` are in scope for refactoring.
- Files in `src/middleware/` and `src/config/` are READ-ONLY. Do not modify.
- Test files in `tests/` may be updated only to reflect moved imports.
## Rules
- Always read and map dependencies before proposing changes.
- Do not modify any file until a refactoring plan has been explicitly approved.
- Commit after each logical refactoring step with a descriptive message.
- Run `npm test` after every file modification.
## Project Context
- Runtime: Node.js 20
- Framework: Express.js 4.18
- Test runner: Jest 29
This configuration declares scope boundaries that Claude Code is instructed to respect. These are not enforced by the tool itself — verify compliance after each step using git diff --name-only. The READ-ONLY declaration for middleware and config files signals that cascading changes outside the intended scope are unwanted, but always confirm via git diff that no out-of-scope files were touched.
If a scoped file path does not exist (e.g., due to a typo), Claude Code will report it as missing rather than silently skipping it — verify paths before starting.
Choosing a Real Refactoring Target
The example used throughout this article is a common scenario: an Express.js route handler that has accumulated inline validation, data transformation, and response formatting logic in a single file. The goal is to extract the validation logic into a dedicated utility module.
First, ensure your supporting files exist. The route handler imports from src/utils/constants.js, which should contain:
module.exports = { TAX_RATE: 0.08, DISCOUNT_THRESHOLD: 100, DISCOUNT_RATE: 0.1 };
The route handler also imports from src/config/database.js, which provides db.orders.create(). That file’s implementation depends on your database setup and is outside the refactoring scope.
Here is the route handler to be refactored:
const express = require('express');
const router = express.Router();
const db = require('../config/database');
const { TAX_RATE, DISCOUNT_THRESHOLD, DISCOUNT_RATE } = require('../utils/constants');
router.post('/orders', async (req, res) => {
const { items, customerEmail } = req.body;
if (!items || !Array.isArray(items) || items.length === 0) {
return res.status(400).json({ error: 'Items must be a non-empty array' });
}
if (!customerEmail || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(customerEmail)) {
return res.status(400).json({ error: 'Valid email is required' });
}
for (let index = 0; index < items.length; index++) {
const item = items[index];
if (!item.sku || !Number.isInteger(item.quantity) || item.quantity < 1) {
return res.status(400).json({ error: `Item at index ${index} has an invalid SKU or quantity` });
}
if (typeof item.price !== 'number' || item.price < 0) {
return res.status(400).json({ error: `Item at index ${index} has an invalid price` });
}
}
const subtotal = items.reduce((sum, i) => sum + (i.price * i.quantity), 0);
const discount = subtotal > DISCOUNT_THRESHOLD ? subtotal * DISCOUNT_RATE : 0;
const total = (subtotal - discount) * (1 + TAX_RATE);
if (!Number.isFinite(total)) {
return res.status(400).json({ error: 'Order total could not be calculated' });
}
try {
const order = await db.orders.create({ items, customerEmail, subtotal, discount, total });
const roundedTotal = parseFloat(total.toFixed(2));
res.status(201).json({ orderId: order.id, total: roundedTotal });
} catch (err) {
console.error('Order creation failed:', err);
res.status(500).json({ error: 'Order creation failed' });
}
});
module.exports = router;
Three distinct responsibilities are tangled together in this file. The validation logic, the price calculation, and the persistence/response handling all live in one function body.
Phase 1: Read and Map Dependencies
Prompting Claude Code to Analyze Before Acting
The first prompt must be explicit about what to do and, critically, what not to do. Vague prompts lead to premature edits.
Read src/routes/orders.js and all files it imports. List every function, variable, and module dependency. Identify which symbols are used across multiple files. Do not modify any files.
The “Do not modify any files” instruction is not redundant. Without it, agentic behavior can lead the tool to start “helpfully” restructuring code as part of its analysis. The CLAUDE.md rules reinforce this, but the prompt-level constraint provides a second layer of protection. Both layers are recommended, but neither is a guaranteed hard stop. CLAUDE.md establishes project intent; the prompt-level instruction reinforces it per request. Always verify via git diff before committing.
After running this prompt, verify no files were modified:
git diff --name-only
Understanding the Dependency Report
Claude Code produces a dependency analysis. The following is an illustrative example of the kind of output you can expect; actual format varies by session and model version:
## Dependency Analysis: src/routes/orders.js
### Direct Imports
- `express` (external package)
- `../config/database` → exports: `db.orders.create()`, `db.orders.find()`
- `../utils/constants` → exports: `TAX_RATE` (0.08), `DISCOUNT_THRESHOLD` (100), `DISCOUNT_RATE` (0.1)
### Symbols Defined in This File
- `router` (Express Router instance) — exported as module.exports
- Inline validation logic (no named functions, not reusable)
- Inline price calculation (references TAX_RATE, DISCOUNT_THRESHOLD, DISCOUNT_RATE)
### Cross-File References
- `TAX_RATE`, `DISCOUNT_THRESHOLD`, and `DISCOUNT_RATE` are also imported by `src/routes/invoices.js`
- `db.orders.create()` is called only from this file
- No other file imports from `src/routes/orders.js`
### Side Effects
- No side effects beyond standard Express router instantiation.
### Safe Refactoring Boundaries
- Validation logic can be extracted without affecting other files
- Price calculation can be extracted; only references constants from utils/constants
- Router setup and db call should remain in this file
This report reveals that the validation and price calculation logic have no external dependents, making them safe extraction targets. The cross-file reference to TAX_RATE, DISCOUNT_THRESHOLD, and DISCOUNT_RATE in invoices.js is important: it confirms these constants should stay in utils/constants rather than being moved.
With the dependency map complete, you now have a clear picture of what can move safely — which feeds directly into the planning phase.
Phase 2: Plan the Refactoring
Generating a Step-by-Step Refactoring Plan
With the dependency map in hand, the next prompt asks for a plan without executing it.
Based on your dependency analysis, propose a step-by-step refactoring plan to extract validation logic from src/routes/orders.js into a new src/utils/validators.js module. Include the exact file changes, new exports, and import rewiring. Do not make any changes yet.
The tool responds with a numbered plan:
## Proposed Refactoring Plan
1. Create `src/utils/validators.js` with three exported functions:
- `validateItems(items)` — returns `{ valid: boolean, error: string | null }`
- `validateEmail(email)` — returns `{ valid: boolean, error: string | null }`
- `validateOrderPayload(body)` — orchestrator that calls both
2. Update `src/routes/orders.js`:
- Add import: `const { validateOrderPayload } = require('../utils/validators')`
- Replace inline validation block (lines 9–24) with a call to `validateOrderPayload`
- No changes to price calculation or persistence logic in this step
3. Update `tests/routes/orders.test.js`:
- Existing validation test cases should still pass (they test via HTTP)
- Add new unit tests for `validators.js` functions directly
4. No changes needed to:
- `src/config/database.js` (read-only)
- `src/utils/constants.js` (no modification required)
- `src/routes/invoices.js` (no shared dependency affected)
Review this plan carefully. If the tool proposed changes to files marked READ-ONLY in CLAUDE.md, that would be a red flag worth pushing back on. In this case, the plan respects the boundaries.
Setting Checkpoints and Rollback Points
Before approving execution, ensure you are on a feature branch with a clean working tree. Run the following to set up:
git checkout -b refactor/extract-validators
git status
If the working tree is not clean, run git stash or commit pending changes before starting the refactoring session.
Instructing Claude Code to commit after each logical step provides rollback points. If step 2 breaks tests, reverting to the commit from step 1 is trivial. This is not unique to AI-assisted refactoring, but it becomes more important when an agentic tool makes changes faster than you can review diffs.
The plan is now approved; Phase 3 turns it into working code.
Phase 3: Execute and Verify
Executing the Refactoring Step by Step
With the plan approved, execution begins with step 1: creating the new validator module.
'use strict';
function validateItems(items) {
if (!items || !Array.isArray(items) || items.length === 0) {
return { valid: false, error: 'Items must be a non-empty array' };
}
for (let index = 0; index < items.length; index++) {
const item = items[index];
if (!item.sku) {
return { valid: false, error: `Item at index ${index} is missing a SKU` };
}
if (!Number.isInteger(item.quantity) || item.quantity < 1) {
return { valid: false, error: `Item at index ${index} has an invalid quantity` };
}
if (typeof item.price !== 'number' || item.price < 0) {
return { valid: false, error: `Item at index ${index} has an invalid price` };
}
}
return { valid: true, error: null };
}
function validateEmail(email) {
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return { valid: false, error: 'Valid email is required' };
}
return { valid: true, error: null };
}
function validateOrderPayload(body) {
const itemsResult = validateItems(body && body.items);
if (!itemsResult.valid) return itemsResult;
return validateEmail(body && body.customerEmail);
}
module.exports = { validateItems, validateEmail, validateOrderPayload };
Note: validateOrderPayload uses short-circuit evaluation — it returns on the first validation failure rather than collecting all errors. This matches the behavior of the original inline code, which also returned on the first failure. If your Express setup does not include body-parser middleware, the body parameter could be undefined; the null-safe access via body && body.items handles that case.
Error messages reference only the item index, not the raw item data, to avoid leaking potentially sensitive fields to clients.
The cleaned-up route handler now delegates validation:
'use strict';
const express = require('express');
const router = express.Router();
const db = require('../config/database');
const { TAX_RATE, DISCOUNT_THRESHOLD, DISCOUNT_RATE } = require('../utils/constants');
const { validateOrderPayload } = require('../utils/validators');
router.post('/orders', async (req, res) => {
const validation = validateOrderPayload(req.body);
if (!validation.valid) {
return res.status(400).json({ error: validation.error });
}
const { items, customerEmail } = req.body;
const subtotal = items.reduce((sum, i) => sum + (i.price * i.quantity), 0);
const discount = subtotal > DISCOUNT_THRESHOLD ? subtotal * DISCOUNT_RATE : 0;
const total = (subtotal - discount) * (1 + TAX_RATE);
if (!Number.isFinite(total)) {
return res.status(400).json({ error: 'Order total could not be calculated' });
}
try {
const order = await db.orders.create({ items, customerEmail, subtotal, discount, total });
const roundedTotal = parseFloat(total.toFixed(2));
res.status(201).json({ orderId: order.id, total: roundedTotal });
} catch (err) {
console.error('Order creation failed:', err);
res.status(500).json({ error: 'Order creation failed' });
}
});
module.exports = router;
Error handling around the database call is essential in production. An unhandled rejection from db.orders.create() will crash the Express process on Node.js versions before 15, or leave the request hanging on later versions. The Number.isFinite guard ensures that a NaN or Infinity total (which could arise from unexpected input combinations) is caught before reaching the database.
The route handler dropped from 27 lines inside the callback to 20 lines, and the validation logic is now independently testable.
Running Tests and Catching Regressions
Running shell commands requires the allowedTools permission to be configured in your Claude Code settings. Confirm that shell execution is permitted before relying on automated test runs.
After each step, the tool should run the test suite. The prompt is direct:
Run npm test. If any tests fail, show me the failure output and suggest a fix. Do not apply the fix until I approve.
This assumes your package.json includes a "test": "jest" script and that Jest is configured to find your test files.
If a test fails because it directly asserts on the error message format and the refactored validators changed the wording, the tool flags it. You can then approve or reject the proposed test update, maintaining control over what constitutes an acceptable behavioral change versus an unintended regression.
Final Review and Cleanup
After all steps complete and tests pass, a final review prompt catches loose ends: “Review all files changed in this session. Check for orphaned imports, unused variables, inconsistent naming, and any references to the old inline validation code.” This sweep catches the kind of debris that accumulates during multi-step refactoring, such as a leftover comment referencing “inline validation” in the route handler or an unused require statement.
The Complete Refactoring Checklist
Without a planning phase, the tool will often propose broader changes than intended because it lacks context about which changes you consider safe. The planning phase is where you apply that judgment.
Read Phase
- ☐ Configure
CLAUDE.md(uppercase, case-sensitive) with refactoring scope and read-only declarations - ☐ Identify the refactoring target and create a feature branch (
git checkout -b refactor/) - ☐ Confirm Claude Code shell execution permissions are enabled via
allowedTools - ☐ Prompt Claude Code to read and map all dependencies. Explicitly instruct it not to modify files.
- ☐ Verify no files were changed (
git diff --name-onlyshould be empty) - ☐ Review the dependency report for shared state and side effects
Plan Phase
- ☐ Request a step-by-step refactoring plan
- ☐ Approve or adjust the plan before proceeding
Execute Phase
- ☐ Execute changes one step at a time with git commits between steps
- ☐ Run tests after each step
- ☐ Fix any regressions before proceeding to the next step
- ☐ Final review: orphaned imports, naming consistency, dead code
- ☐ Squash or organize commits for PR submission (e.g.,
git rebase -i HEAD~Nwhere N is the number of refactoring commits)
Common Pitfalls and How to Avoid Them
Letting Claude Code Change Files During the Read Phase
You will notice unintended edits when an analysis request gets interpreted as an invitation to “fix” things. You prevent this by combining two layers of constraint: the CLAUDE.md rule (“Do not modify any file until a refactoring plan has been explicitly approved”) and the prompt-level instruction (“Do not modify any files”). Both layers are recommended, but neither is a guaranteed hard stop. CLAUDE.md establishes project intent; the prompt-level instruction reinforces it per request. Always verify via git diff before committing.
Refactoring Too Many Files at Once
Context window limits are a real constraint. When the tool tracks changes across too many files simultaneously, it produces inconsistent edits — updating an import in one file but missing the same import in another. As a rough heuristic, estimate the total token count of all files in scope and keep it under 40% of your model’s context limit. If you cannot estimate tokens easily, cap a single session at 5 files or fewer. When you notice missed imports or inconsistent edits, split the work into sequential sessions. Larger refactors should follow their own read-map-plan-execute cycle per session.
Skipping the Planning Phase
Jumping from dependency analysis directly to execution prevents you from catching scope creep, rejecting unnecessary changes, or reordering steps for safety. Without a planning phase, the tool will often propose broader changes than intended because it lacks context about which changes you consider safe. The planning phase is where you apply that judgment.
A structured workflow restores that deliberate sequencing, ensuring that reading always precedes writing.

