🚨 The Refactoring Problem at Scale

Imagine you're working on a React application with a shared design system. Your team decides to update the Button component API to be more consistent:

// Before <Button type="primary" size="large" disabled={false} /> // After <Button variant="primary" size="lg" disabled={false} />

In a small project, updating these props is straightforward. Your IDE can find and replace type= with variant= and size="large" with size="lg" across a few components in minutes.

But now imagine this design system is used across multiple applications, dozens of teams, and hundreds of components. Suddenly, this simple prop rename becomes a nightmare:

  • 🔍 Hidden references — props aren't always in JSX; they may be destructured, e.g. const { type, size } = buttonProps or function MyButton({ type, size }) { ... }, which a simple find-and-replace won't catch.
  • 🎯 Logic dependencies — business logic often relies on prop values, e.g. if (type === "primary") { ... }, so updating JSX alone breaks behavior.
  • 🏭 Generated props — factories or helpers can create props dynamically, e.g. const buttonProps = createButtonProps({ type: "primary" }), meaning the change isn't always local.
  • 💥 Unsafe replacements — global search risks false positives, like accidentally updating unrelated code such as const user = { type: "admin" }.
  • Cross-team overhead — even if you fix your app, dozens of other teams are making changes in parallel, causing endless merge conflicts and weeks of coordination.

What started as a simple design system update is now a complex, error-prone, and time-consuming migration affecting your entire organization.

This is exactly why codemods exist: to automate safe, large-scale code transformations across codebases that are too big and complex for manual refactors.

🎛️ What Are Codemods?

A codemod (short for code modification) is a scripted transformation of source code. Instead of manually searching and replacing code, a codemod analyzes your code's Abstract Syntax Tree (AST) and rewrites it in a safe and predictable way. Think of them as "search & replace on steroids" — but instead of working with raw text, they understand your code's structure, context, and meaning.

Unlike simple find-and-replace operations, codemods follow a sophisticated 4-step process:

  • 🧠 Code Analysis: Codemods analyze the codebase by parsing it into an Abstract Syntax Tree (AST). The AST represents the code's structure in a way that's easy for computers to understand and manipulate.
  • 🔍 Pattern Matching: Codemods then traverse the AST, searching for specific code patterns or structures that need to be modified.
  • Transformation: Once a match is found, the codemod applies the necessary changes to the AST. This could involve adding, deleting, or replacing code elements.
  • 📝 Code Regeneration: Finally, the codemod transforms the modified AST back into readable source code, effectively updating the codebase.

This process allows codemods to perform complex transformations that would be impossible with simple text-based find-and-replace operations.

🤩 Use-Cases for Codemods

Codemods are a proven tool for reducing technical debt and evolving codebases safely. They turn risky migrations into automated, repeatable processes. Here are some of the most impactful ways teams use them:

🔼 Framework & Library Upgrades

Frameworks evolve quickly, and upgrades often bring breaking API changes. Codemods make those migrations fast and reliable.

  • 👉 Next.js publishes official codemods to help developers migrate between major versions (e.g., 14 → 15), turning what could take weeks into a script that runs in minutes.
  • 👉 AWS also provides a codemod to migrate from aws-sdk v2 to v3, handling massive API changes without requiring teams to manually rewrite every single call.

✅ With codemods, painful upgrades become routine maintenance.

⚙️ Tooling Migrations

Switching test frameworks or developer tools is notoriously tedious. Codemods remove the pain.

  • 👉 Entire organizations have migrated from Mocha/Tape → Jest, with codemods automatically rewriting imports, test syntax, and assertions.
  • 👉 This keeps thousands of tests consistent and eliminates weeks of manual effort.

✅ Codemods let teams change their tools without slowing down development.

React React & UI Transformations

React's fast evolution would have left developers behind — if not for codemods.

👉 Facebook used codemods internally (and later shared them publicly) to:

  • Replace deprecated lifecycle methods.
  • Migrate to Hooks.
  • Convert PropTypes to TypeScript interfaces.

✅ Thanks to codemods, these massive refactors scaled across millions of lines of code without breaking development velocity.

🔄 Modernization & Consistency

Beyond upgrades, codemods are a powerful way to modernize legacy code and enforce consistent practices.

👉 Companies like Airbnb and Stripe have used codemods to:

  • Replace callback patterns with async/await.
  • Restructure imports for better tree-shaking.
  • Adopt TypeScript by automatically inserting type annotations.

✅ This keeps teams aligned, codebases modern, and reviews focused on logic.

🛠️ Creating Your First Codemod

Now that we've seen what codemods are and some scenarios where they shine, it's time to take the next step: creating our own codemod.

In this section, we'll walk through the process of building a simple codemod to convert CommonJS require() statements to ES6 import statements. This is a common migration task that many teams face when modernizing their JavaScript codebases.

// Before const fs = require("fs"); const path = require("path"); const { readFile, writeFile } = require("fs/promises"); // After import fs from "fs"; import path from "path"; import { readFile, writeFile } from "fs/promises";

But before we dive into the own codemod solution, let's first see what tools we have available to help us write codemods effectively.

🧰 Codemod Tools

Codemods exist for many programming languages (Java, Python, Go, etc.), but in this case we'll focus on JavaScript/TypeScript. Within the JavaScript ecosystem, there are several powerful libraries for writing codemods, let's review some of most popular:

JavaScript jscodeshift

  • 🏢 Created by Facebook and battle-tested on massive codebases.
  • 👥 Most popular codemod toolkit with extensive community support.
  • 💻 Works with both JavaScript and TypeScript.
  • 📚 Good documentation and examples.

🔎 Read more about jscodeshift here.

TypeScript ts-morph

  • 🔷 TypeScript-first approach with excellent type awareness.
  • 🎯 More intuitive API for developers familiar with TypeScript.
  • 🔧 Great for complex type transformations and refactoring.
  • 💪 Smaller community but very powerful for TS-specific tasks.

🔎 Read more about ts-morph here.

✨ Our chosen solution

Both tools are excellent, but for our case, we'll use jscodeshift because:

  • 📚 Learning curve: Extensive documentation and real-world examples make it beginner-friendly.
  • 🏢 Industry standard: Used by Facebook, Airbnb, and countless other companies.
  • 🔄 Flexibility: Handles both simple and complex transformations equally well.
  • 🌍 Community: Largest ecosystem of existing codemods you can learn from and build upon.

📥 Setting Up Our Project

Now that we've chosen jscodeshift as our codemod tool, let's set up a new project to create our codemod.

First, we need to create a new directory for our codemod project. Open your terminal and run the following commands:

mkdir require-to-import-codemod cd require-to-import-codemod

Next, we need to initialize a new npm project. This will create a package.json file where we can manage our dependencies:

npm init -y

Now, we need to install jscodeshift as a dependency. This is the library that will help us write and run our codemod:

npm install jscodeshift

Note that here, we are using jscodeshift as a local dependency which is my recommended approach for custom codemods, since if we are working on a large codebase with multiple teams, we avoid potential conflicts with globally installed versions.

Finally, let's create a folder where we can have some test files to run our codemod against:

mkdir test-files

And let's create a sample file example.js inside test-files with some CommonJS require() statements:

JavaScripttest-files/example.js
const fs = require('fs'); const path = require('path'); const { readFile, writeFile } = require('fs/promises'); const express = require('express'); // Dynamic requires should stay unchanged const dynamicModule = require(moduleName);

Now we have our project set up with jscodeshift installed and a test file to work with. We are ready to write our codemod logic.

🪄 Writing the Transformation Logic

Let’s write our codemod in a new file named require-to-import.js that will contain the transformation logic. This file will use jscodeshift's API to traverse the AST of our JavaScript files and replace require() statements with import statements.

JavaScriptrequire-to-import.js
/** * Checks if an AST node represents a static require() call with a string literal. * * @param {Object} node - AST node to check * @returns {boolean} True if node is a static require() call * * @example * // ✅ Matches these patterns: * const fs = require('fs'); * const { readFile } = require('fs/promises'); * * // ❌ Does NOT match dynamic requires: * const mod = require(variableName); */ function isStaticRequireCall(node) { return ( node?.type === 'CallExpression' && node.callee.name === 'require' && node.arguments[0]?.type === 'Literal' ); } /** * Creates an ES module default import statement from a CommonJS require. * * @param {Object} j - jscodeshift API * @param {Object} identifierNode - AST node for the variable identifier * @param {string} moduleSpecifier - Module name/path to import from * @returns {Object} Default import AST node * * @example * // Transforms: const fs = require('fs'); * // Into: import fs from 'fs'; */ function createDefaultImportStatement(j, identifierNode, moduleSpecifier) { return j.importDeclaration( [j.importDefaultSpecifier(j.identifier(identifierNode.name))], j.literal(moduleSpecifier) ); } /** * Creates an ES module named import statement from a CommonJS require with destructuring. * * @param {Object} j - jscodeshift API * @param {Object} destructuringPattern - AST node for the object destructuring pattern * @param {string} moduleSpecifier - Module name/path to import from * @returns {Object} Named import AST node * * @example * // Transforms: const { readFile, writeFile } = require('fs/promises'); * // Into: import { readFile, writeFile } from 'fs/promises'; */ function createNamedImportStatement(j, destructuringPattern, moduleSpecifier) { const importSpecifiers = destructuringPattern.properties .filter( prop => prop.type === 'Property' && prop.key.type === 'Identifier' ) .map(prop => j.importSpecifier(j.identifier(prop.key.name))); return j.importDeclaration(importSpecifiers, j.literal(moduleSpecifier)); } /** * Converts a require() variable declarator to the appropriate import statement. * * @param {Object} j - jscodeshift API * @param {Object} variableDeclarator - AST node for the variable declaration * @returns {Object|null} Import statement AST node, or null if conversion not possible * * @example * // Handles both default and named imports: * // const fs = require('fs') → import fs from 'fs' * // const { readFile } = require('fs') → import { readFile } from 'fs' */ function convertRequireToImport(j, variableDeclarator) { const moduleSpecifier = variableDeclarator.init.arguments[0].value; const leftHandSide = variableDeclarator.id; const isDefaultImport = leftHandSide.type === 'Identifier'; const isNamedImport = leftHandSide.type === 'ObjectPattern'; if (isDefaultImport) { return createDefaultImportStatement(j, leftHandSide, moduleSpecifier); } if (isNamedImport) { return createNamedImportStatement(j, leftHandSide, moduleSpecifier); } // Return null for unsupported patterns (e.g., array destructuring) return null; } /** * Codemod transformer to convert CommonJS require statements into ES module imports. * * @param {Object} fileInfo - File information including source code * @param {Object} api - jscodeshift API * @returns {string} Transformed source code * * Supported transformations: * - Default imports: const fs = require('fs') → import fs from 'fs' * - Named imports: const { readFile } = require('fs') → import { readFile } from 'fs' * * Ignores: * - Dynamic requires: require(variableName) * - Non-const declarations: let/var require statements */ module.exports = function transformer(fileInfo, api) { // Initialize jscodeshift and parse the source code into an AST const j = api.jscodeshift; // j is the jscodeshift API helper for manipulating the AST. const sourceRoot = j(fileInfo.source); // sourceRoot is the parsed AST of the file’s source code. // Find all const declarations that might contain require() calls const constVariableDeclarations = sourceRoot .find(j.VariableDeclaration) .filter(path => path.value.kind === 'const'); // Process each const declaration constVariableDeclarations.forEach(declarationPath => { declarationPath.value.declarations.forEach(variableDeclarator => { // Skip if not a static require() call if (!isStaticRequireCall(variableDeclarator.init)) return; // Convert to import statement const importStatement = convertRequireToImport(j, variableDeclarator); if (!importStatement) return; // Replace the entire const declaration with import j(declarationPath).replaceWith(importStatement); }); }); // Convert the modified AST back to JavaScript code using single quotes return sourceRoot.toSource({ quote: 'single' }); };

At first glance, a codemod written with jscodeshift can look a bit intimidating—there are many APIs and AST concepts that may not be obvious right away. But once you get comfortable with how the API works, you'll realize it gives you incredible power to perform safe, large-scale refactors and migrations that would otherwise be error-prone or impossible manually.

In this example, we've aimed to be very descriptive about every step of the transformation. Each small piece of logic is separated into atomic helper functions, and we've used JSDoc comments to clearly explain what each function does, including examples of the input and expected output. This approach makes the codemod easier to read, maintain, and extend, while also serving as a self-documenting reference for anyone looking at the code later.

🚀 Running Our Codemod

At this point, we have our codemod logic ready. Now we can test it against our test-files/example.js.

For doing it, we can use the --dry and --print flags to preview the changes without modifying the file:

./node_modules/.bin/jscodeshift -t require-to-import.js test-files/example.js --dry --print

If everything looks good, we are ready to run the codemod:

./node_modules/.bin/jscodeshift -t require-to-import.js test-files/example.js

Your test-files/example.js should now show:

JavaScripttest-files/example.js
import fs from "fs"; import path from "path"; import { readFile, writeFile } from "fs/promises"; import express from "express"; // Dynamic requires stay unchanged const dynamicModule = require(moduleName);

In this example, we're running the codemod manually on a single file (test-files/example.js) to see how it works. However, jscodeshift allows you to run it over an entire directory or even filter by specific file extensions.

💡 Best Practices

Now that you know how to create a basic codemod, here are some best practices to keep in mind when building and using codemods in your projects:

  • 🧪 Test in a safe environment – Run your codemod on a small subset of your codebase or a dedicated branch before applying it everywhere.
  • 📄 Document your codemods – Explain what the codemod does, why it exists, and how it should be used for future team members.
  • 📚 Document your codemod internally – Codemods can be hard to understand at first. To make them clearer, use JSDoc comments to describe what we are doing, and break your logic into small, descriptive functions wherever it helps readability and maintainability.
  • 🔁 Make codemods idempotent – Running them multiple times shouldn't break the code or create duplicate changes.
  • 🧠 Leverage type information and AST parsing – Avoid relying solely on text search; understand the structure of your code to make safe transformations.

🎯 Conclusion

In this way, we've seen how, with the use of codemods, we can automate large-scale changes in our code safely and efficiently, reducing manual errors and speeding up refactors in large projects.

We've also covered best practices that I've applied in some of my own codemods, such as creating small and descriptive functions, documenting behavior with JSDoc, and structuring the code clearly, which makes it easier to understand, maintain, and reuse in the future.

I hope this post has been helpful and inspires you to explore the power of codemods in your own projects.

Thank you for reading! 🙌