Skip to content

Large-Scale Refactoring from the Terminal

You have been putting off this refactoring for six months. The utils directory has 47 files. Half the codebase uses callbacks while the other half uses async/await. There are three different ways to handle errors. Your team lead finally said “we need to clean this up before the next feature push” and you drew the short straw.

Manual refactoring at scale is tedious and error-prone. You rename a function, miss one call site, and the bug report arrives at 3 PM on Friday. Claude Code changes this equation. It reads your entire codebase, understands the dependency graph, and applies changes systematically across hundreds of files. Combined with fan-out patterns for parallel execution and hooks for automatic verification, large-scale refactoring becomes a weekend task instead of a month-long slog.

  • A workflow for safe, large-scale refactoring with automatic test verification
  • Fan-out patterns using headless mode for codebase-wide transformations
  • Prompts for common refactoring tasks: renames, pattern migrations, module extraction
  • The strangler fig approach for incremental replacement of legacy patterns

Large refactoring fails when you change everything at once, break tests in ways you do not understand, and spend more time debugging the refactoring than you spent on the refactoring itself. The safe approach is incremental: one pattern at a time, verified at every step.

  1. Establish the safety net

    Before changing any code, make sure the tests pass and you have a clean git state.

    Run the full test suite and confirm everything passes.
    Then run the linter and type checker. If anything is broken
    already, fix it before we start refactoring. I need a clean
    baseline to refactor against.
  2. Analyze what needs to change

  3. Refactor one batch at a time

    Start with batch 1: the utility functions in src/lib/.
    For each file:
    1. Make the change
    2. Update all callers of the changed code
    3. Run the tests for the affected modules
    Show me the test output after each file. If a test fails,
    fix it before moving to the next file.
  4. Commit each batch independently

    All tests pass for batch 1. Commit with message:
    "Migrate src/lib/ utilities from callbacks to async/await"
  5. Repeat for the next batch

    Continue until all batches are complete. Each batch is committed independently, so you can bisect if something breaks later.

For truly large-scale changes — renaming a type across 200 files, updating import paths after a directory restructure, or migrating a deprecated API — headless mode lets you parallelize the work.

Terminal window
# Find all files that use the old pattern
grep -rl "oldFunctionName" src/ | while read file; do
claude -p "In $file, rename all occurrences of oldFunctionName
to newFunctionName. Update any related variable names and
comments. Do not change the logic, only the names." &
done
wait
# Run tests to verify
npm test

For more complex transformations:

Inside an interactive session, sub-agents can handle different parts of the refactoring simultaneously:

Use sub-agents to refactor these three areas in parallel:
1. Migrate all files in src/services/ from the old error
handling pattern to the new AppError pattern
2. Migrate all files in src/routes/ from express callbacks
to async route handlers
3. Update all files in src/middleware/ to use the new
logger instead of console.log
Each sub-agent should run the relevant tests after making
changes. Report back with the results.
Rename the User model field "fname" to "firstName" everywhere:
1. Update the database migration
2. Update the Prisma/Drizzle schema
3. Update all service files that reference the field
4. Update all API response shapes
5. Update all test files
6. Update all frontend components that display the field
Search for both "fname" and "user.fname" and "user['fname']"
to catch all access patterns. Run the full test suite after
all changes.
The authentication logic is scattered across six files. Extract
it into a self-contained src/modules/auth/ directory:
1. First, identify every auth-related function, type, and
constant across the codebase
2. Create the new directory structure:
- src/modules/auth/index.ts (public API)
- src/modules/auth/service.ts
- src/modules/auth/middleware.ts
- src/modules/auth/types.ts
- src/modules/auth/constants.ts
3. Move the code, updating all imports across the codebase
4. Ensure the public API of the module is explicit -- only
export what external code actually uses
Run tests after every move to catch broken imports immediately.
Convert src/legacy/ from JavaScript to TypeScript incrementally.
Start with the leaf files (no dependencies on other legacy files),
then work inward.
For each file:
1. Rename .js to .ts
2. Add type annotations for all function parameters and returns
3. Replace any with proper types
4. Add interfaces for object shapes
5. Fix any type errors the compiler finds
Do not change any logic. This is a type-only migration.
Run tsc --noEmit after each file to catch type errors immediately.
Our dependency @acme/sdk just released v3 which deprecates
the query() method in favor of execute(). The migration guide
says:
- query(sql) becomes execute({ sql })
- query(sql, params) becomes execute({ sql, params })
- The return type changed from rows[] to { rows, metadata }
Find every file that imports from @acme/sdk and uses query().
Apply the migration. Update the return type handling.
Run tests after each file.

For legacy systems where you cannot refactor everything at once, use the strangler fig pattern: build the new version alongside the old one, gradually route traffic to it, and remove the old code once everything is migrated.

We need to replace our homegrown validation with Zod.
Current validation is spread across 30+ files using custom
validate() functions.
Phase 1 (this PR):
- Create Zod schemas that match the current validation rules
- Add a compatibility wrapper that runs both old and new
validation and logs discrepancies
- Deploy and monitor for mismatches
Phase 2 (next PR):
- Switch to Zod as the primary validator
- Keep the old code as a fallback behind a feature flag
Phase 3 (final PR):
- Remove the old validation code and the feature flag
- Remove the compatibility wrapper
Start with Phase 1. Read three existing validate() functions
to understand the current rules, then create equivalent Zod
schemas.

Set up hooks that run after every edit to catch problems immediately:

.claude/settings.json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "npx tsc --noEmit 2>&1 | head -20"
}
]
}
}

For refactoring that affects many files, a broader check after each edit prevents cascading errors:

After every file you change, run:
1. npx tsc --noEmit (catch type errors immediately)
2. npm test -- --run --reporter=dot (quick test run)
If either fails, stop and fix before moving to the next file.
Do not accumulate broken files.

After completing a refactoring, quantify what improved:

Compare the codebase before and after this refactoring:
1. Run the linter and compare warning counts
2. Run the type checker and compare error counts
3. Run the test suite and compare pass/fail/duration
4. Count the number of files, functions, and lines of code
5. Measure cyclomatic complexity for the refactored modules
Show me a before/after summary so I can include it in the PR
description.

Refactoring breaks a test you did not expect. The change had a hidden dependency. Roll back to the last commit, investigate the dependency, and update the plan to account for it. This is why committing after each batch matters — rollbacks are clean.

The rename missed some call sites. Claude’s search missed dynamic access patterns like obj[fieldName] or string interpolation. After a rename, run a broader search: “Search for both the old name and any string that contains the old name. Check configuration files, SQL queries, and template strings.”

The refactoring is too big for one session. Use headless mode for the mechanical parts and save the interactive session for the complex transformations. Or split the refactoring into multiple PRs: “Give me a plan that breaks this refactoring into three PRs, each independently deployable and reviewable.”

Type errors cascade after a change. You changed a type that is used everywhere. Start from the type definition and work outward: “Fix the type errors starting from the files closest to the changed type, then move to the files that depend on those files. Show me the error count after each file.”

The team is still merging code into the files you are refactoring. Coordinate with the team. For large refactorings, consider a short code freeze on the affected files, or use git worktrees so you can rebase frequently without losing progress.

With clean, well-structured code and comprehensive tests, you are in a strong position to generate documentation that stays current with the codebase.