Refactoring Legacy JavaScript with OpenAI Codex
By Sumit Saha
Refactor legacy JavaScript and Node.js code with OpenAI Codex using a safe workflow: map undocumented logic, lock behavior with tests, then modernize CommonJS, callbacks, and outdated patterns step by step. Learn a practical AI-assisted refactoring process that improves maintainability without risky full rewrites.
![]()
Legacy code is rarely scary because it is old. It is scary because nobody wants to be the person who touches it and breaks production. That is the real bottleneck. Not syntax. Not frameworks. Not even missing documentation. The real problem is uncertainty.
This is where OpenAI Codex becomes useful. Not as a magical "rewrite my repo" button, but as a sharp co-pilot that helps you understand messy code, preserve existing behavior, and modernize one safe step at a time.
In this guide, we will walk through a practical workflow for using Codex to refactor undocumented JavaScript and Node.js codebases. The goal is not cosmetic cleanup. The goal is to turn fragile, high-risk code into something clear, testable, and maintainable without losing the business logic that still keeps the system alive.
Table of contents
- The legacy code bottleneck
- Install and verify the tools
- Use AI for comprehension before you refactor
- Lock behavior before you change structure
- Refactor one seam at a time
- Optimize for modern standards
- The future of co-pilot maintenance
- Recap
The legacy code bottleneck
Every team eventually inherits a file that feels like a black box. It works. Kind of. Nobody fully understands it. There are nested callbacks, strange variable names, hidden side effects, and maybe a comment from six years ago that says "do not touch". So nobody does.
The cost of that code grows quietly.
- New features take longer because every change feels risky.
- Bug fixes are slower because cause and effect are buried.
- Onboarding hurts because the code explains nothing.
- Refactoring gets postponed until the system becomes harder to move.
This is why legacy code drains velocity. It forces good engineers into defensive mode.
Codex helps by changing the first step. Instead of immediately editing code, you use AI to map the terrain first. You ask for summaries, dependency clues, risky areas, and likely behavior. That alone changes the game because fear usually comes from not seeing the system clearly.
Warning: Do not let AI jump straight into large-scale rewrites on day one. In legacy systems, understanding comes before modernization.
Install and verify the tools
Before you use Codex on a real repository, make sure the basic tooling is ready.
Install and verify Node.js and npm
Node.js gives you the runtime for JavaScript tooling, and npm is the package manager you will use to install the Codex CLI.
A common approach is to install Node.js from the official installer or use a version manager such as nvm if you work across multiple projects.
After installation, verify that both commands are available:
node -v
npm -vYou should see version numbers printed in the terminal.
Install and verify OpenAI Codex CLI
The Codex CLI lets you work with Codex directly from your terminal inside a local repository. It is ideal when you want Codex to inspect files, explain code, suggest edits, and help you step through refactors.
Install it globally with npm:
npm i -g @openai/codexNow verify the installation:
codex --versionYou should see a Codex version string in the output.
Once installed, move into the repository you want to work on and launch Codex:
cd path/to/your-legacy-project
codexOn the first run, sign in with your ChatGPT account or configure an API key if that is the path you prefer.
Tip: Start Codex from the root of the repository, not from a random subfolder. Better context leads to better analysis.
Use AI for comprehension before you refactor
This is where most people waste time.
They open a legacy file, scroll for ten minutes, get frustrated, and start changing code before they understand the shape of the system. That is how regressions happen.
A better pattern is to let Codex act like the senior engineer who just joined the project and is doing a fast but structured codebase review.
Start with prompts that force explanation, not implementation.
codex "Explain this codebase like a senior engineer taking over the repo. Map the main modules, entry points, side effects, and the highest-risk files."Then narrow the scope.
codex "Open the legacy billing flow and tell me what this module actually does, what it depends on, what can break if it changes, and which parts are safe to refactor first."Then make the hidden behavior visible.
codex "Find undocumented assumptions in this file. List implicit inputs, output formats, mutation points, and any external systems this code touches."These prompts do three important things:
- They turn spaghetti into a map.
- They expose invisible coupling.
- They identify seams where safe refactoring can begin.
In old systems, the first win is not "clean code". The first win is clarity.
Lock behavior before you change structure
A dangerous refactor is not a refactor. It is a gamble.
Before touching business logic, use Codex to help you preserve the current behavior with tests. Even if the code is ugly, even if the naming is terrible, even if the architecture is outdated, your first job is to lock down what the system currently does.
Here is a small example of the kind of legacy module you might inherit:
// legacy/normalize-status.js
function normalizeStatus(input) {
if (input === "paid" || input === "PAYMENT_RECEIVED") return "paid";
if (input === "pending" || input === null || input === undefined)
return "pending";
return "unknown";
}
module.exports = { normalizeStatus };Now ask Codex to generate behavior-preserving tests before proposing changes:
codex "Write focused tests for legacy/normalize-status.js that preserve the current behavior exactly. Do not refactor the implementation yet."You can also write them yourself using Node's built-in test runner:
// test/normalize-status.test.js
const test = require("node:test");
const assert = require("node:assert/strict");
const { normalizeStatus } = require("../legacy/normalize-status");
test("normalizes paid states", () => {
assert.equal(normalizeStatus("paid"), "paid");
assert.equal(normalizeStatus("PAYMENT_RECEIVED"), "paid");
});
test("normalizes pending states", () => {
assert.equal(normalizeStatus("pending"), "pending");
assert.equal(normalizeStatus(null), "pending");
assert.equal(normalizeStatus(undefined), "pending");
});
test("falls back to unknown", () => {
assert.equal(normalizeStatus("refunded"), "unknown");
});Run the tests:
node --testOnce those tests pass, you now have something priceless: a safety net.
Tip: In legacy refactoring, tests are not just about quality. They are about courage.
Refactor one seam at a time
This is where Codex shines.
You do not want a giant rewrite. You want a sequence of small, reviewable moves. Each move should preserve behavior while improving one part of the system.
Migrate CommonJS to ES Modules
Many older Node.js projects are full of require() and module.exports. Modernizing to ES Modules improves consistency and makes the code easier to reason about in newer stacks.
Here is a simple legacy example:
const fs = require("node:fs");
const path = require("node:path");
function readConfig() {
const filePath = path.join(process.cwd(), "config.json");
return JSON.parse(fs.readFileSync(filePath, "utf8"));
}
module.exports = { readConfig };A targeted Codex prompt keeps the task narrow:
codex "Convert this module from CommonJS to ES Modules without changing behavior. Keep the public API equivalent and flag any import path changes that might affect the rest of the repo."A modernized result might look like this:
import fs from "node:fs";
import path from "node:path";
export function readConfig() {
const filePath = path.join(process.cwd(), "config.json");
return JSON.parse(fs.readFileSync(filePath, "utf8"));
}That might look small, but do not underestimate what matters here. The value is not the syntax swap. The value is that you did it deliberately, with scope control and review.
Turn callbacks into async and await
Callback-heavy code is one of the biggest readability killers in older Node.js systems.
Here is a simplified example:
const fs = require("node:fs");
const path = require("node:path");
function loadUser(id, callback) {
fs.readFile(
path.join(__dirname, "users.json"),
"utf8",
function (err, data) {
if (err) return callback(err);
let users;
try {
users = JSON.parse(data);
} catch (parseError) {
return callback(parseError);
}
const user = users.find(function (item) {
return item.id === id;
});
callback(null, user || null);
},
);
}
module.exports = { loadUser };This works, but it is mentally expensive.
Now ask Codex for a behavior-preserving rewrite:
codex "Refactor this callback-based function into async and await. Preserve behavior, preserve error handling, and do not change the returned data shape."A cleaner version:
const fs = require("node:fs/promises");
const path = require("node:path");
async function loadUser(id) {
const filePath = path.join(__dirname, "users.json");
const data = await fs.readFile(filePath, "utf8");
const users = JSON.parse(data);
const user = users.find((item) => item.id === id);
return user || null;
}
module.exports = { loadUser };The business logic is the same. The reading experience is not.
Replace brittle patterns with modern Node.js APIs
Legacy code often contains patterns that made sense years ago but now create unnecessary friction.
This is where Codex can help you audit the codebase for:
- callback-based filesystem logic that should move to promise-based APIs
- repeated utility code that can be replaced with built-in platform features
- manual parsing or transformation code that can be simplified
- inconsistent error handling paths
- duplicated business rules spread across multiple files
Prompt it like this:
codex "Scan this module for outdated Node.js patterns and suggest the smallest modern replacements that improve readability and reliability without changing behavior."Then review every suggestion like an architect, not a spectator.
Warning: "Modern" is not automatically "correct". The right refactor is the one that preserves intent, not the one that looks newest.
Optimize for modern standards
Once the most painful legacy patterns are under control, the next job is reducing future maintenance cost.
This is where you start asking sharper questions:
- Which logic is duplicated in multiple places?
- Which functions do too many things?
- Which variable names hide business meaning?
- Which flows need stricter input validation?
- Which modules should become pure and testable?
- Which files should gain lightweight type safety through JSDoc or TypeScript migration later?
At this stage, Codex is useful as a pattern detector.
codex "Find repeated logic in this service layer, group similar branches, and suggest a smaller architecture that reduces duplication without changing external behavior."codex "Review this refactored code for missed edge cases, weak naming, missing validation, and security risks introduced during cleanup."That last prompt matters. AI can help you modernize, but it can also introduce subtle mistakes if you accept everything at face value. Your role does not disappear. It becomes more important.
You stop being the person who manually types every line and become the person who sets direction, constraints, and review standards.
A useful practical rule is this:
- Use Codex to widen visibility.
- Use tests to lock behavior.
- Use small refactors to improve structure.
- Use human review to protect correctness.
That is the workflow.
The future of co-pilot maintenance
The real opportunity is not one heroic cleanup sprint.
The real opportunity is building a repeatable maintenance habit.
Once you trust your workflow, Codex can help you with ongoing code health tasks such as:
- summarizing unfamiliar pull requests before review
- identifying risky files before feature work starts
- proposing test coverage for older modules
- surfacing duplicate logic across service layers
- auditing error handling and edge cases
- preparing small modernization tasks that fit inside normal sprint work
A simple operating rhythm could look like this:
codex "Review the last set of changes in this repo and list technical debt that should be cleaned up next, ranked by risk and payoff."codex "Identify one low-risk refactor in this codebase that improves readability without changing behavior. Explain why it is safe."codex "Find modules with weak tests or no tests, and suggest the smallest useful test plan to reduce regression risk."This is how AI becomes more than a novelty. It becomes part of a maintenance system.
Not a replacement for engineering judgment. A force multiplier for it.
Recap
Legacy code slows teams down because it hides intent and amplifies fear.
OpenAI Codex helps most when you use it in the right order:
- Understand the code before editing it.
- Lock behavior with tests.
- Refactor one seam at a time.
- Review every AI-generated change with discipline.
- Turn one-off cleanup into a repeatable maintenance workflow.
That is how you unlock value from old JavaScript and Node.js systems without betting the whole codebase on a rewrite.
The code may be legacy. Your process does not have to be.
Show your Support
- ⭐ Follow this GitHub Repo
- 🍿 Subscribe on YouTube
- 🧑🏫 Follow us on LinkedIn, Facebook and X