# AI-assisted code migration: how to switch frameworks safely

> Coding agents can handle repetitive migrations, but switching costs also include design drift, production behavior, deployment, and future maintenance.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-10-20 | Topics: [AI](https://flaviocopes.com/tags/ai/) | Canonical: https://flaviocopes.com/ai-migrations/

A coding agent can make a framework migration look surprisingly easy.

Give it a component, tell it to replace one styling library with another, and it can produce a plausible result. Give it the whole project and enough time, and it can change hundreds of files while you work on something else.

That changes the economics of software maintenance. Work you once postponed because it was tedious can become affordable.

It also creates a temptation to migrate more often. A library feels old, a new runtime looks cleaner, or another developer posts an impressive before-and-after. The code changes seem cheap enough to try.

I would still begin with the problem the migration is supposed to solve. An agent can reduce the effort of changing the code. It cannot decide whether the new stack will make the product easier to maintain a year from now.

## Repetitive work was a real barrier

Think about replacing a CSS framework in an established application.

The visible work includes changing class names, removing imports, and adjusting components. But the same patterns repeat across the project. A form field appears on several screens. A modal follows a convention that every feature copies.

That repetition makes the work a good candidate for automation. Once you establish the desired result for a representative component, an agent can apply the pattern elsewhere.

The potential benefit is larger than typing speed. You can ask it to find every use of the old framework, identify custom overrides, and group the affected pages. It can give you a map before making changes.

For a small team, that can make an upgrade possible without abandoning product work for a week. It can also make it practical to maintain a compatibility layer while replacing things gradually.

I would use that ability to reduce the size of each step. Asking for the entire migration in one pass throws away one of the best advantages of having help with tedious work.

## A component can compile and still be wrong

Suppose a Bulma form becomes a Tailwind form.

Both can produce a text input with a label. The migrated component renders. The build passes. A quick screenshot looks reasonable.

But what happened to the focus indicator? Does the error message still describe the input for a screen reader? Does a long label wrap correctly? Is the disabled state readable? Does the form fit on a narrow phone?

A styling migration has no universal one-to-one mapping because frameworks make different layout and design choices. Tailwind's [utility documentation](https://tailwindcss.com/docs/styling-with-utility-classes) explains a composition model. Bulma's [form documentation](https://bulma.io/documentation/form/) describes its own component conventions. Replacing a class name does not prove that those conventions produce equivalent behavior.

You need to decide what must stay the same.

If this is a technical migration, preserve the existing appearance closely. If you also want a redesign, define that as part of the work. Otherwise, the agent will make hundreds of small visual decisions and you will discover the new design while reviewing the diff.

That is a difficult moment to establish what you wanted.

## Write down the reason to switch

I would want a short explanation that survives after the migration is complete.

The old dependency may block a framework upgrade. You may spend too much time maintaining its overrides, or want one styling approach across several products.

Those are concrete reasons. You can check whether the change achieved them.

A vague desire for a modern stack is harder to evaluate. The project may end up with a large diff while the maintenance problems survive under a different dependency.

The reason also defines the stopping point. If the goal is removing a vulnerable package, you do not need to rewrite unrelated components. If the goal is faster builds, measure build time on the same project and machine before making the change.

An agent's ability to keep working should not become the project's definition of success. Give it a result that you can inspect.

For example, replacing one abandoned dependency while preserving behavior is a bounded task. Modernizing the entire application invites changes to routing, state management, naming, and architecture that may have nothing to do with the original problem.

## Make the current behavior visible

Before changing a component, save enough evidence to compare it later.

For a styling migration, I would capture representative pages at desktop and mobile widths. I would include validation errors, empty lists, and long content. The pleasant default state is usually the least revealing one.

For a runtime migration, I would list the commands that matter. Starting development is one command. Building production output, running scheduled work, and stopping the server cleanly are different behaviors.

Existing tests help, but inspect what they cover. A test that mocks the database cannot tell you whether the new runtime loads the production database driver. A screenshot of a full table cannot tell you whether the empty state still appears.

This preparation can be small. You do not need to build a new testing platform before changing a dependency. You need a few checks that describe what users rely on.

I would ask the agent to identify missing coverage around the migration boundary. Then I would choose the checks that protect actual behavior, instead of accepting a large new test suite that mostly confirms the rewritten code exists.

## Start with a slice that can fail cheaply

A good first slice includes a real difficulty.

For CSS, I would choose a page with a form, a responsive layout, and an error state. Migrating a simple heading proves very little.

For a server runtime, I would choose a route that uses the database and an external service. A health endpoint that returns a string is useful for deployment checks, but it cannot reveal most compatibility problems.

Ask the agent to change that slice, then stop. Review the result in the browser or run it in a production-like environment. Read the diff while it is small enough to understand.

You will learn which instructions were missing. Maybe the agent introduced a new component abstraction for no reason. Maybe it changed spacing everywhere because the new library's defaults looked nicer. Correct the example before letting it repeat those choices.

A small completed slice becomes a reference the agent can follow. It also gives you evidence for abandoning the migration early if the supposed simplification requires more work than expected.

## Runtime compatibility needs narrower claims

Deno is a useful example because adopting it can mean several different things.

You can evaluate its tooling, use its package management, or run an application under its runtime. Those choices have different consequences.

The official [Node.js migration guide](https://docs.deno.com/runtime/migrate/) describes incremental adoption and support for existing `package.json` projects. Deno's [Node compatibility documentation](https://docs.deno.com/runtime/fundamentals/node/) also lists the compatibility details you need to check.

I would treat those documents as a starting point for a project-specific trial. Support for npm packages does not establish that every package in your application works with your exact deployment setup.

Native dependencies deserve attention. So do install scripts, filesystem assumptions, and subprocesses. Deno's [security model](https://docs.deno.com/runtime/fundamentals/security/) introduces explicit permissions that need to match the application's work.

My [Deno introduction](https://flaviocopes.com/deno/) covers the runtime itself. For a migration, the more useful question is which part of your current setup you want Deno to replace, and how you will verify that part.

## Keep deployment in the task

A migration can work locally and fail after the first production restart.

Your laptop may have a binary the container lacks. A development server can also hide assumptions about writable directories, while the hosting platform starts the application with a different command.

The agent needs to inspect those assumptions before declaring the work complete.

I would include the production build and startup path in the first trial. If deployment uses a container, build that container. If the app runs on a managed runtime, use its preview environment and check the real integrations there.

Pay attention to shutdown and background work too. A successful HTTP response does not tell you whether a queued job finishes, a scheduled task runs once, or the process exits cleanly during a deployment.

These details often live outside the source folder the agent is editing. That is why a migration request should describe the running system, not just the framework package to replace.

## The diff should explain itself

I would split mechanical changes from changes in behavior.

If a migration replaces imports, that is easy to review as one kind of change. If it also redesigns error handling and renames database fields, the reviewer has several problems mixed together.

Ask the agent to leave unrelated cleanup alone. Old names can survive another week. A migration is already enough work to reason about.

Review repeated changes by pattern, then inspect the exceptions carefully. The unusual component with a custom override is more likely to break than the twentieth ordinary button.

Also look for compatibility code that became permanent by accident. An agent may add a wrapper to get one package working, then forget to explain why it exists. Keep the wrapper only if the benefit of the migration still justifies maintaining it.

The best outcome is often fewer dependencies and clearer ownership. If the new setup needs several custom bridges, check whether you have transferred maintenance work from an upstream project into your own repository.

## Reverting code does not always revert the migration

A Git revert can restore source files. It cannot necessarily undo what the new application wrote while it was running.

A CSS migration usually has a cheap rollback. A database or storage migration may not.

If a new release changes saved data, the old release needs to understand that data during rollback. Otherwise, reverting the deployment can make the application less usable.

For persistent changes, I would plan compatibility before switching traffic. Add the new representation, allow both application versions to work during the transition, and remove the old representation only after it is safe. The exact sequence depends on the system, but the rollback question belongs at the beginning.

An agent can help write the migration and the reverse operation. You still need to establish what happens to writes made between them.

This is also why I would avoid combining a framework migration with a database redesign. Each deserves its own evidence and recovery plan.

## Rehearse the rollback with a new write

Imagine that moving a notes app to a new framework also changes its saved content. The old application stores Markdown. The new editor stores a JSON document.

That is a data migration hidden inside an interface change. An agent could convert every existing note correctly and still leave you unable to roll back after someone edits a note in production.

First, migrate the framework while keeping Markdown as the saved format. That gives the framework change its own acceptance test: create a note, edit it, reload it, then open it in the previous application release. Its content should survive unchanged.

For the later format change, I would use the [expand-and-contract pattern](https://www.prisma.io/docs/guides/database/data-migration). Keeping the old column is only useful if its contents remain usable during the transition.

For the notes app, I would begin by checking whether every supported editor action can round-trip through Markdown. If it can, keep the Markdown representation updated while both releases may run. If the editor introduces features Markdown cannot represent, disable those features during the rollback window or choose a different recovery plan before launch.

Then rehearse the case that a conversion script misses. Deploy the new release to a test environment. Create a note there, edit an older note, and switch back to the previous release without restoring the database. Verify both notes using the old interface.

A database backup gives you another recovery option, but restoring it can discard writes made afterward. I would want an explicit answer about those writes before treating a backup as the rollback plan.

The agent can automate this rehearsal. Give it the expected content and the two application releases to exercise. A report that the old release still reads newly edited notes tells you more than a report that the conversion command finished.

## Decide what you will maintain afterward

Once the code works, someone has to own the new stack.

Can you debug a production failure without asking the agent to explain the entire application? Can another developer run it? Are the build commands documented? Do you understand which parts came from the framework and which parts are your own compatibility layer?

Those questions matter even when the migration took very little of your time.

I would accept a migration when it removes a problem I can name, preserves the behavior I care about, and leaves a setup I am willing to maintain. A smaller bill for repetitive code changes makes more migrations worth considering. It does not make every new dependency worth adopting.

For a project I wanted to move, my next step would be one representative page or one real server route, with the old behavior recorded beside it. I would decide whether to continue after using that result.
