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


Photo of Juntao QIU | 邱俊涛

Juntao is a software engineer at Atlassian with a passion for Test-Driven Development, refactoring, and clean code. He enjoys sharing his knowledge and helping other developers grow.

As an author, blogger, YouTuber, and content creator, Juntao provides practical guidance to help developers write better code. He also writes and regularly updates The Pragmatic Developer, a newsletter dedicated to pragmatic software practices.


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:

  1. Parsing the code into an AST, where each part of the code is represented as a tree structure.
  2. Modifying the tree by applying a transformation, such as renaming a function or changing parameters.
  3. 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 Extract Function, Rename Variable, or Inline Function. Essentially, your IDE parses the source code into ASTs and applies predefined transformations to the tree, saving the result back into your files.

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 Change Function Declaration, where you can adjust the order of parameters or default values before finalizing the change.

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:

  1. Find all instances of featureToggle.
  2. Verify that the argument passed is 'feature-new-product-list'.
  3. 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 calls featureToggle('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.

We're releasing this article in installments. Future installments will talk about more involved situations, and tools such as OpenRewrite to do this in other languages.

To find out when we publish the next installment subscribe to this site's RSS feed, or Martin's feeds on Mastodon, Bluesky, LinkedIn, or X (Twitter).


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”