Claude Code Refactoring: A Practical Workflow
SitePoint

Claude Code Refactoring: A Practical Workflow

Setting Up Your Refactoring Session

Prerequisites

Before starting, confirm the following:

  • Node.js 20.x is installed (node --version should return v20.x.x).
  • Claude Code CLI is installed and authenticated. Run claude --version to confirm. Shell execution permissions must be enabled via the allowedTools configuration in your Claude Code settings file (typically ~/.claude/settings.json) for Claude Code to run commands like npm test on your behalf.
  • Git is initialized in your project root.
  • Your project has a package.json with Express and Jest as dependencies. Run npm list express jest to 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:

// src/utils/constants.js
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:

// src/routes/orders.js
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;

  // Inline validation
  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.name || typeof item.name !== 'string') {
      return res.status(400).json({ error: `Item ${index}: name is required and must be a string` });
    }
    if (!item.price || typeof item.price !== 'number' || item.price <= 0) {
      return res.status(400).json({ error: `Item ${index}: price must be a positive number` });
    }
    if (!item.quantity || typeof item.quantity !== 'number' || item.quantity <= 0) {
      return res.status(400).json({ error: `Item ${index}: quantity must be a positive number` });
    }
  }

  // Price calculation
  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' });
  }

  // Persistence and response
  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
# Expected output: empty (no files modified)

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 the plan, create a clean rollback point:

git checkout -b refactor/extract-validators
git commit -m "checkpoint: pre-refactoring snapshot"

This branch gives you a clean rollback point. If something goes wrong during execution, you can reset to this commit without losing work.

Comments

No comments yet. Start the discussion.