Legacy code gets a bad reputation. It is working software that has survived production, handled edge cases no one documented, and earned its complexity honestly. But working with legacy code can be painful — especially when there are no tests, no documentation, and the original authors are long gone.
Over the past year, I have used Claude Code extensively to modernize legacy codebases. Not as a magic wand that rewrites everything at once, but as a careful partner that helps me understand, test, and incrementally improve code. Here is what I have learned.
Step Zero: Understand Before You Touch
The biggest mistake when refactoring legacy code is jumping straight into changes. With Claude Code, there is no excuse for skipping the understanding phase.
When I encounter an unfamiliar module, I start by asking Claude Code to explain it:
claude "Explain the data flow in src/services/orderProcessing.js —
what are the inputs, outputs, side effects, and error paths?"
Claude Code reads the entire file (and its imports), then produces a structured explanation. This is orders of magnitude faster than tracing through callback chains manually. For deeply nested legacy code with implicit dependencies, this step alone saves hours.
Pro tip: Ask specifically about side effects. Legacy code loves hidden mutations — global state changes, database writes buried in utility functions, event emitters that trigger cascading updates. Claude Code is excellent at surfacing these.
Create a CLAUDE.md for Legacy Projects
Before doing any refactoring work, I create a CLAUDE.md file at the project root. This is Claude Code's project context file, and for legacy projects it is invaluable:
# CLAUDE.md — Legacy Order System
## Architecture
- Express.js monolith, no framework conventions
- PostgreSQL via raw `pg` queries (no ORM)
- Background jobs via node-cron (see src/jobs/)
## Known Gotchas
- `req.user` is populated by custom middleware in src/auth/legacy-auth.js
- The `processOrder` function in src/services/orders.js has side effects:
it sends emails AND writes to the audit log
- Environment variables are accessed directly (no config module)
## Refactoring Rules
- Never change function signatures that are used by the API layer
- Always add tests before modifying a function
- Prefer async/await over callbacks in new code
This file acts as institutional knowledge that persists across sessions. Every time Claude Code works on this project, it reads CLAUDE.md first and respects the constraints. I update it as I learn more about the codebase.
The Golden Rule: Tests Before Changes
Legacy code without tests is a minefield. Before refactoring anything, I use Claude Code to generate test coverage for the existing behavior:
claude "Write integration tests for the processOrder function in
src/services/orders.js. Cover the happy path, invalid input,
and the case where the email service is down.
Use Jest. Mock the database and email service."
Claude Code reads the implementation, identifies the branches, and generates tests that capture the current behavior — including the quirks. These tests become my safety net. If a refactoring breaks something, the tests catch it immediately.
This is the single most important pattern I have found: test the existing behavior first, then refactor with confidence.
Safe Refactoring Strategies
Callbacks to async/await
Legacy Node.js code is often a pyramid of callbacks. Claude Code handles this migration pattern reliably:
// Before: callback hell
function getUser(id, callback) {
db.query('SELECT * FROM users WHERE id = $1', [id], (err, result) => {
if (err) return callback(err);
const user = result.rows[0];
db.query('SELECT * FROM profiles WHERE user_id = $1', [user.id], (err2, profileResult) => {
if (err2) return callback(err2);
user.profile = profileResult.rows[0];
callback(null, user);
});
});
}
// After: clean async/await
async function getUser(id) {
const userResult = await db.query('SELECT * FROM users WHERE id = $1', [id]);
const user = userResult.rows[0];
const profileResult = await db.query('SELECT * FROM profiles WHERE user_id = $1', [user.id]);
user.profile = profileResult.rows[0];
return user;
}
The key is to do this one function at a time and run the tests after each change. Claude Code understands the caller chain and can tell you which callers need to be updated.
Class Components to Hooks
For React legacy codebases, migrating class components to functional components with hooks is a common task:
claude "Refactor src/components/Dashboard.jsx from a class component
to a functional component with hooks. Preserve all behavior
including the componentDidMount API call, the shouldComponentUpdate
optimization, and the error boundary."
Claude Code handles this.state to useState, componentDidMount to useEffect, and even suggests when useMemo or useCallback are appropriate replacements for shouldComponentUpdate. It also warns when something cannot be directly migrated — like error boundaries, which still require class components.
Extracting Configuration
Legacy code often has hardcoded values scattered everywhere. I use Claude Code to find and extract them:
claude "Find all hardcoded URLs, ports, timeouts, and magic numbers in
src/services/. Create a config.ts module that centralizes them
with environment variable overrides."
This is a task that would take hours of manual grep and is error-prone. Claude Code searches systematically and produces a clean config module.
Dealing with No Documentation
Legacy projects rarely have useful documentation. Claude Code can generate it from the code itself:
claude "Generate API documentation for all Express routes in src/routes/.
Include the HTTP method, path, expected request body, query parameters,
authentication requirements, and possible response codes.
Format as Markdown."
I commit this generated documentation and then manually verify and correct it. Even if it is 80% accurate, it saves enormous time compared to starting from scratch.
Incremental Over Big-Bang
The temptation with AI tools is to attempt massive rewrites. Resist this. I follow a strict incremental approach:
- Understand one module (ask Claude Code to explain it)
- Test the existing behavior (generate tests with Claude Code)
- Refactor one pattern at a time (callbacks, naming, structure)
- Verify tests still pass
- Document what changed and why in the commit message
- Repeat for the next module
Each step is a small, reviewable commit. If something goes wrong, you can revert one commit instead of untangling a massive diff.
What Claude Code Does Not Replace
Claude Code is not a substitute for understanding your system. It accelerates comprehension, but you still need to:
- Verify its explanations — it can misinterpret unusual patterns
- Make architectural decisions — it can suggest options, but you own the direction
- Understand the business context — why the code does something matters as much as what it does
Legacy code modernization is a marathon, not a sprint. Claude Code makes each step faster and less risky, but the discipline of small, tested, incremental changes is still on you.
Conclusion
Claude Code has fundamentally changed how I approach legacy codebases. The combination of AI-assisted comprehension, test generation, and incremental refactoring means I can modernize code confidently — even code I have never seen before. The key is patience: understand first, test second, refactor third. No shortcuts.


