Modernizing Legacy Codebases with Claude Code
The Problem
Your codebase is years old. It uses jQuery, CommonJS modules, callback-based async patterns, and has no TypeScript. Dependency versions are outdated with known vulnerabilities. No tests exist, so every change feels risky. You need to modernize incrementally without stopping feature development and without breaking production.
Quick Start
Ask Claude Code to assess the current state:
Analyze this legacy codebase and create a modernization report:
1. Node.js/runtime version and compatibility issues
2. Outdated dependencies with known vulnerabilities
3. Code patterns that need updating (callbacks, var, CommonJS)
4. Missing type safety (no TypeScript)
5. Test coverage (or lack thereof)
6. Estimated effort for each modernization area
Prioritize by risk reduction and developer experience improvement.
What’s Happening
Legacy code modernization is one of Claude Code’s strongest use cases because it requires understanding existing code deeply, planning a safe migration path, and making systematic changes across many files. Claude Code can:
- Read and understand old patterns (jQuery, Backbone, AngularJS, CommonJS)
- Plan incremental migration paths that keep the app running at every step
- Make systematic find-and-replace transformations across hundreds of files
- Add TypeScript types to existing JavaScript code
- Generate tests for untested code before refactoring it
- Verify each change by running the application
Step-by-Step Guide
Step 1: Audit the codebase
Ask Claude Code to produce a detailed audit:
Read the project structure, package.json, and sample files from each
directory. Create a modernization audit covering:
- Language: JS vs TS, module system, ES version
- Framework: jQuery/Backbone/Angular/React version
- Build tools: Grunt/Gulp/Webpack/Vite
- Package manager: npm/yarn/pnpm, lockfile present?
- Node.js version requirements
- Test framework and coverage
- Linting and formatting tools
- Known vulnerability count (npm audit)
Step 2: Add tests before refactoring
Never refactor without tests. Ask Claude Code to add tests to the most critical code paths first:
Add tests for the payment processing module (src/payments/).
This code has no tests. Write integration tests that verify:
- Successful payment creates a transaction record
- Failed payment returns the correct error
- Duplicate payment IDs are rejected
- Refund processing updates the transaction status
Use the existing patterns (if any) or set up Jest from scratch.
Claude Code reads the existing code, understands the logic, and generates tests that verify current behavior. This creates a safety net for refactoring.
Step 3: Upgrade Node.js version
Assess compatibility with Node.js 20 LTS. Check for:
- Dependencies that don't support Node 20
- Native modules that need rebuilding
- Deprecated APIs (url.parse, crypto.createCipher)
- Changes needed in package.json engines field
Create a migration plan with specific file changes.
Common issues Claude Code catches:
// Deprecated: url.parse
const parsed = url.parse(myUrl);
// Modern: URL constructor
const parsed = new URL(myUrl);
// Deprecated: crypto.createCipher
const cipher = crypto.createCipher('aes-256-cbc', key);
// Modern: crypto.createCipheriv
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
Step 4: Convert CommonJS to ES Modules
Convert this project from CommonJS to ES Modules incrementally.
Start with utility files that have no dependencies, then work up
to the main entry points.
For each file:
1. Change require() to import
2. Change module.exports to export
3. Add .js extensions to relative imports (required for ESM)
4. Update package.json type field
5. Run tests after each file to verify nothing breaks
Before:
const express = require('express');
const { UserService } = require('./services/user');
const config = require('../config');
module.exports = function createRouter() {
const router = express.Router();
// ...
return router;
};
After:
import express from 'express';
import { UserService } from './services/user.js';
import config from '../config.js';
export function createRouter() {
const router = express.Router();
// ...
return router;
}
Step 5: Add TypeScript incrementally
Add TypeScript to the project without converting all files at once.
1. Install TypeScript and configure tsconfig.json with allowJs: true
2. Convert src/types/ to TypeScript first (create type definitions)
3. Convert utility files (src/utils/) next
4. Convert service layer (src/services/) next
5. Leave route handlers for last (most complex)
Each converted file should use strict types, not 'any'.
TypeScript config for incremental adoption:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"allowJs": true,
"checkJs": false,
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
Step 6: Replace callbacks with async/await
Find all callback-based async patterns in the codebase and convert
them to async/await. Handle these patterns:
1. Callback pyramids (nested callbacks)
2. Event emitter patterns that should be promises
3. fs callbacks (use fs/promises)
4. Database query callbacks
Preserve error handling — every try/catch should match the original error paths.
Before:
function getUser(id, callback) {
db.query('SELECT * FROM users WHERE id = ?', [id], function(err, rows) {
if (err) return callback(err);
if (rows.length === 0) return callback(new Error('Not found'));
db.query('SELECT * FROM orders WHERE user_id = ?', [id], function(err, orders) {
if (err) return callback(err);
rows[0].orders = orders;
callback(null, rows[0]);
});
});
}
After:
async function getUser(id: string): Promise<UserWithOrders> {
const rows = await db.query('SELECT * FROM users WHERE id = ?', [id]);
if (rows.length === 0) {
throw new Error('Not found');
}
const orders = await db.query('SELECT * FROM orders WHERE user_id = ?', [id]);
return { ...rows[0], orders };
}
Step 7: Replace jQuery with modern JavaScript
For frontend legacy code:
Convert jQuery DOM manipulation to vanilla JavaScript or React.
Start with the simplest components and work toward complex ones.
For each jQuery usage:
- $.ajax → fetch API
- $(selector).on('click') → addEventListener
- $.each → Array.forEach/map
- $(selector).html() → element.innerHTML or React state
- $(selector).show/hide() → CSS classes or conditional rendering
Step 8: Update build tools
Migrate from Webpack 4 to Vite:
1. Identify all Webpack-specific features in use (loaders, plugins)
2. Map each to the Vite equivalent
3. Create a vite.config.ts that replaces webpack.config.js
4. Update all import paths for Vite compatibility
5. Update package.json scripts
6. Test the dev server and production build
Modernization Order
The safest order for modernization:
- Add tests (safety net for everything else)
- Upgrade Node.js (enables modern syntax)
- Add TypeScript (catches bugs during refactoring)
- Convert to ES Modules (modern import system)
- Replace callbacks (cleaner async code)
- Update framework (jQuery to React, etc.)
- Update build tools (Webpack to Vite, etc.)
- Update dependencies (now that the code is modern)
Each step should be a separate PR with passing tests.
Prevention
Add modernization standards to your CLAUDE.md:
## Code Standards
- TypeScript strict mode (no any, no ts-ignore)
- ES Modules (import/export, not require/module.exports)
- Async/await (no callbacks for async operations)
- Modern APIs (fetch, URL, fs/promises)
- Every new file must have tests
- Every refactored file must have tests added before refactoring