Refactoring with Codemods to Automate API Changes
Refactoring is something developers do all the time—making code easier to understand, maintain, and extend. While IDEs can handle simple refactorings with just a few keystrokes, things get tricky when you need to apply changes across large or distributed codebases, especially those you don’t fully control. That’s where codemods come in. By using Abstract Syntax Trees (AST), codemods allow you to automate large-scale code changes with precision and minimal effort, making them especially useful when dealing with breaking API changes. This article looks at how codemods can help manage these challenges, with practical examples like removing feature toggles or refactoring complex React components. We’ll also discuss potential pitfalls and how to avoid them when using codemods at scale.
07 January 2025
As a library developer, you may create a popular utility that hundreds of thousands of developers rely on daily, such as lodash or React. Over time, usage patterns might emerge that go beyond your initial design. When this happens, you may need to extend an API by adding parameters or modifying function signatures to fix edge cases. The challenge lies in rolling out these breaking changes without disrupting your users’ workflows.
This is where codemods come in—a powerful tool for automating large-scale code transformations, allowing developers to introduce breaking API changes, refactor legacy codebases, and maintain code hygiene with minimal manual effort.
In this article, we’ll explore what codemods are and the tools you can use to create them, such as jscodeshift, hypermod.io, and codemod.com. We’ll walk through real-world examples, from cleaning up feature toggles to refactoring component hierarchies. You’ll also learn how to break down complex transformations into smaller, testable pieces—a practice known as codemod composition—to ensure flexibility and maintainability.
By the end, you’ll see how codemods can become a vital part of your toolkit for managing large-scale codebases, helping you keep your code clean and maintainable while handling even the most challenging refactoring tasks.
Breaking Changes in APIs
Returning to the scenario of the library developer, after the initial release, new usage patterns emerge, prompting the need to extend an
For simple changes, a basic find-and-replace in the IDE might work. In
more complex cases, you might resort to using tools like sed
or awk
. However, when your library is widely adopted, the
scope of such changes becomes harder to manage. You can’t be sure how
extensively the modification will impact your users, and the last thing
you want is to break existing functionality that doesn’t need
updating.
A common approach is to announce the breaking change, release a new version, and ask users to migrate at their own pace. But this workflow, while familiar, often doesn't scale well, especially for major shifts. Consider React’s transition from class components to function components with hooks—a paradigm shift that took years for large codebases to fully adopt. By the time teams managed to migrate, more breaking changes were often already on the horizon.
For library developers, this situation creates a burden. Maintaining multiple older versions to support users who haven’t migrated is both costly and time-consuming. For users, frequent changes risk eroding trust. They may hesitate to upgrade or start exploring more stable alternatives, which perpetuating the cycle.
But what if you could help users manage these changes automatically? What if you could release a tool alongside your update that refactors their code for them—renaming functions, updating parameter order, and removing unused code without requiring manual intervention?
That’s where codemods come in. Several libraries, including React and Next.js, have already embraced codemods to smooth the path for version bumps. For example, React provides codemods to handle the migration from older API patterns, like the old Context API, to newer ones.
So, what exactly is the codemod we’re talking about here?
What is a Codemod?
A codemod (code modification) is an automated script used to transform code to follow new APIs, syntax, or coding standards. Codemods use Abstract Syntax Tree (AST) manipulation to apply consistent, large-scale changes across codebases. Initially developed at Facebook, codemods helped engineers manage refactoring tasks for large projects like React. As Facebook scaled, maintaining the codebase and updating APIs became increasingly difficult, prompting the development of codemods.
Manually updating thousands of files across different repositories was inefficient and error-prone, so the concept of codemods—automated scripts that transform code—was introduced to tackle this problem.
The process typically involves three main steps:
- Parsing the code into an AST, where each part of the code is represented as a tree structure.
- Modifying the tree by applying a transformation, such as renaming a function or changing parameters.
- Rewriting the modified tree back into the source code.
By using this approach, codemods ensure that changes are applied consistently across every file in a codebase, reducing the chance of human error. Codemods can also handle complex refactoring scenarios, such as changes to deeply nested structures or removing deprecated API usage.
If we visualize the process, it would look something like this:
Figure 1: The three steps of a typical codemod process
The idea of a program that can “understand” your code and then perform
automatic transformations isn’t new. That’s how your IDE works when you
run refactorings like
For modern IDEs, many things happen under the hood to ensure changes
are applied correctly and efficiently, such as determining the scope of
the change and resolving conflicts like variable name collisions. Some
refactorings even prompt you to input parameters, such as when using
Use jscodeshift in JavaScript Codebases
Let’s look at a concrete example to understand how we could run a codemod in a JavaScript project. The JavaScript community has several tools that make this work feasible, including parsers that convert source code into an AST, as well as transpilers that can transform the tree into other formats (this is how TypeScript works). Additionally, there are tools that help apply codemods to entire repositories automatically.
One of the most popular tools for writing codemods is jscodeshift, a toolkit maintained by Facebook. It simplifies the creation of codemods by providing a powerful API to manipulate ASTs. With jscodeshift, developers can search for specific patterns in the code and apply transformations at scale.
You can use jscodeshift
to identify and replace deprecated API calls
with updated versions across an entire project.
Let’s break down a typical workflow for composing a codemod manually.
Clean a Stale Feature Toggle
Let’s start with a simple yet practical example to demonstrate the power of codemods. Imagine you’re using a feature toggle in your codebase to control the release of unfinished or experimental features. Once the feature is live in production and working as expected, the next logical step is to clean up the toggle and any related logic.
For instance, consider the following code:
const data = featureToggle('feature-new-product-list') ? { name: 'Product' } : undefined;
Once the feature is fully released and no longer needs a toggle, this can be simplified to:
const data = { name: 'Product' };
The task involves finding all instances of featureToggle
in the
codebase, checking whether the toggle refers to
feature-new-product-list
, and removing the conditional logic surrounding
it. At the same time, other feature toggles (like
feature-search-result-refinement
, which may still be in development)
should remain untouched. The codemod needs to understand the structure
of the code to apply changes selectively.
Understanding the AST
Before we dive into writing the codemod, let’s break down how this specific code snippet looks in an AST. You can use tools like AST Explorer to visualize how source code and AST are mapped. It’s helpful to understand the node types you're interacting with before applying any changes.
The image below shows the syntax tree in terms of ECMAScript syntax. It
contains nodes like Identifier
(for variables), StringLiteral
(for the
toggle name), and more abstract nodes like CallExpression
and
ConditionalExpression
.
Figure 2: The Abstract Syntax Tree representation of the feature toggle check
In this AST representation, the variable data
is assigned using a
ConditionalExpression
. The test part of the expression calls
featureToggle('feature-new-product-list')
. If the test returns true
,
the consequent branch assigns { name: 'Product' }
to data
. If
false
, the alternate branch assigns undefined
.
For a task with clear input and output, I prefer writing tests first, then implementing the codemod. I start by defining a negative case to ensure we don’t accidentally change things we want to leave untouched, followed by a real case that performs the actual conversion. I begin with a simple scenario, implement it, then add a variation (like checking if featureToggle is called inside an if statement), implement that case, and ensure all tests pass.
This approach aligns well with Test-Driven Development (TDD), even if you don’t practice TDD regularly. Knowing exactly what the transformation's inputs and outputs are before coding improves safety and efficiency, especially when tweaking codemods.
With jscodeshift, you can write tests to verify how the codemod behaves:
const transform = require("../remove-feature-new-product-list"); defineInlineTest( transform, {}, ` const data = featureToggle('feature-new-product-list') ? { name: 'Product' } : undefined; `, ` const data = { name: 'Product' }; `, "delete the toggle feature-new-product-list in conditional operator" );
The defineInlineTest
function from jscodeshift allows you to define
the input, expected output, and a string describing the test's intent.
Now, running the test with a normal jest
command will fail because the
codemod isn’t written yet.
The corresponding negative case would ensure the code remains unchanged for other feature toggles:
defineInlineTest( transform, {}, ` const data = featureToggle('feature-search-result-refinement') ? { name: 'Product' } : undefined; `, ` const data = featureToggle('feature-search-result-refinement') ? { name: 'Product' } : undefined; `, "do not change other feature toggles" );
Writing the Codemod
Let’s start by defining a simple transform function. Create a file
called transform.js
with the following code structure:
module.exports = function(fileInfo, api, options) { const j = api.jscodeshift; const root = j(fileInfo.source); // manipulate the tree nodes here return root.toSource(); };
This function reads the file into a tree and uses jscodeshift’s API to
query, modify, and update the nodes. Finally, it converts the AST back to
source code with .toSource()
.
Now we can start implementing the transform steps:
- Find all instances of
featureToggle
. - Verify that the argument passed is
'feature-new-product-list'
. - Replace the entire conditional expression with the consequent part, effectively removing the toggle.
Here’s how we achieve this using jscodeshift
:
module.exports = function (fileInfo, api, options) { const j = api.jscodeshift; const root = j(fileInfo.source); // Find ConditionalExpression where the test is featureToggle('feature-new-product-list') root .find(j.ConditionalExpression, { test: { callee: { name: "featureToggle" }, arguments: [{ value: "feature-new-product-list" }], }, }) .forEach((path) => { // Replace the ConditionalExpression with the 'consequent' j(path).replaceWith(path.node.consequent); }); return root.toSource(); };
The codemod above:
- Finds
ConditionalExpression
nodes where the test callsfeatureToggle('feature-new-product-list')
. - Replaces the entire conditional expression with the consequent (i.e.,
{ name: 'Product' }
), removing the toggle logic and leaving simplified code behind.
This example demonstrates how easy it is to create a useful transformation and apply it to a large codebase, significantly reducing manual effort.
You’ll need to write more test cases to handle variations like
if-else
statements, logical expressions (e.g.,
!featureToggle('feature-new-product-list')
), and so on to make the
codemod robust in real-world scenarios.
Once the codemod is ready, you can test it out on a target codebase, such as the one you're working on. jscodeshift provides a command-line tool that you can use to apply the codemod and report the results.
$ jscodeshift -t transform-name src/
After validating the results, check that all functional tests still pass and that nothing breaks—even if you're introducing a breaking change. Once satisfied, you can commit the changes and raise a pull request as part of your normal workflow.
Codemods Improve Code Quality and Maintainability
Codemods aren’t just useful for managing breaking API changes—they can significantly improve code quality and maintainability. As codebases evolve, they often accumulate technical debt, including outdated feature toggles, deprecated methods, or tightly coupled components. Manually refactoring these areas can be time-consuming and error-prone.
By automating refactoring tasks, codemods help keep your codebase clean and free of legacy patterns. Regularly applying codemods allows you to enforce new coding standards, remove unused code, and modernize your codebase without having to manually modify every file.
Acknowledgements
A heartfelt thanks to Martin Fowler for his generous review and invaluable suggestions to make this article more readable, and for providing a home for long-form articles like this one.
Special thanks to Daniel Del Core, the original author of the Avatar refactoring described in this article and a passionate advocate for large-scale refactoring through tools like hypermod.io.
I’d also like to acknowledge the Atlassian Design System team and my Thoughtworks colleagues from a few years ago, when I had the privilege of working with them on codemods. It was a formative experience where we learned to scale a design system across extensive codebases with the help of codemods.
Significant Revisions
07 January 2025: published first installment, up to “Codemods Improve Code Quality and Maintainability”