Back to blog
Engineering15 min read

25 AI Prompts for Refactoring Legacy Code (Safely)

25 copy-paste AI prompts for refactoring legacy code, grouped by transformation, plus the safety net to build before you touch a line without tests.

NH
Nafiul Hasan
Founder, Prompt Architects

TL;DR: Refactoring legacy code with AI is only safe if you build a safety net first: characterization tests, one named transformation at a time, and a reviewable diff instead of a rewrite. Below are 25 copy-paste prompts organized by transformation type, plus a risk table showing exactly what to have in place before you run each one.

How do you use AI prompts to refactor legacy code safely?

You ask for one transformation at a time, you ask for a diff instead of a rewrite, and you never combine a structural change with a behavioral one in the same step. Before any of that, you need characterization tests capturing what the code actually does today, not what it's supposed to do. Skip that step and you're not refactoring, you're gambling with code nobody, including the AI, fully understands.

This matters more here than in almost any other prompting context. A bad ChatGPT marketing prompt wastes ten minutes. A bad refactor prompt on a legacy payment-processing function ships silently, and you find out three weeks later when a support ticket comes in.

Why is legacy code a different problem than a normal refactor?

Legacy code has a specific definition, and it's not "old." Michael Feathers, in Working Effectively with Legacy Code, defines it plainly: legacy code is code without tests. "It doesn't matter how well written it is; it doesn't matter how pretty or object-oriented or well-encapsulated it is. With tests, we can change the behavior of our code quickly and verifiably." Without them, you're changing behavior blind (source, accessed August 2026).

Three problems compound in real legacy codebases, and each changes how you should prompt:

  • No tests. There's nothing to catch a regression except a human reading a diff carefully, which doesn't scale past a few lines.
  • No docs. You can't ask "what is this supposed to do" and get an answer, only "what does this currently do," which is exactly what a characterization test captures.
  • The original author is gone. Nobody can explain why that odd branch exists. Treat it as intentional until proven otherwise; a branch that looks dead may be handling a customer segment you've never heard of. If you need to build a mental model of unfamiliar code before changing it, prompting AI to explain an unfamiliar codebase is a separate, earlier step from refactoring it. Do that first.

None of this means legacy code is unrefactorable. It means the refactor has to build its own safety net on the way in, because one wasn't left for you.

What do you need in place before AI touches legacy code?

A characterization test, at minimum, for the exact function or class you're about to change. Feathers' term for it: "a test that characterizes the actual behavior of a piece of code." You run the code, capture what it currently returns for a set of inputs, and turn that into an assertion (source, accessed August 2026). You are not asserting correctness. You are asserting sameness, so any future change that breaks it gets caught immediately.

For code that's too tangled to unit-test even at the function level, use a golden master (also called approval testing): capture the full output of the system for a broad set of real inputs, save it as a reference file, and diff future runs against it. The term was coined by Llewellyn Falco, and it's specifically popular for exactly this situation: getting legacy code under test based on its output, without having to understand the internals first (source, accessed August 2026).

How do you ask AI for a diff instead of a rewrite?

Name the exact prompt template transformation you want (extract, rename, inline, collapse) and say explicitly that behavior must not change. Paste only the unit in question, not the surrounding file. A smaller context window gives the model less room to "improve" things you didn't ask about, and it's the difference between a reviewable five-line diff and a 200-line rewrite you have to reverse-engineer.

A weak prompt: "Clean up this function." A working prompt: "Extract the validation logic in lines 12–24 into a separate function named validateOrderPayload. Do not change any other logic, formatting, or variable names outside that block. Show the change as a diff."

The second version is boring. That's the point: boring is reviewable, and reviewable is what makes a refactor safe on code nobody fully understands anymore. If you want a broader gate before any of this, one that checks a change didn't introduce new bugs it wasn't asked to fix, pair this with a genuinely useful AI code review prompt on every diff before you merge it.

One more failure mode worth naming: a model can hallucinate a behavior it thinks the code "should" have, especially around error handling and edge cases, and quietly patch that in alongside the refactor you asked for. Characterization tests are what catch this: a hallucinated behavior change fails the test even when the diff looks reasonable.

Which refactor type is safe to attempt first?

Renaming and extracting small functions are safest to try first: neither can change control flow, and both are easy to verify by reading the diff. Riskier transformations, untangling callbacks or breaking apart a god class, need a bigger safety net before you start. Match the net to the risk using the table below.

Refactor typeRisk levelWhat to have in place first
Rename (variable, function, field)LowA project-wide search confirming every call site; an IDE rename is safer than an AI-generated one
Extract functionLow–MediumOne characterization test on the extracted block's inputs and outputs
Introduce a seam for testingLow–MediumNothing extra; this step creates your safety net, so do it early and often
Collapse duplicationMediumCharacterization tests on both duplicates, confirming they genuinely behave identically today
Modernize idioms (callbacks to promises, var to const)MediumA characterization test on outer behavior, since async timing can shift subtly even when logic doesn't
Tighten typesMedium–HighFull characterization coverage of every call site the type touches, plus a passing type-check
Replace conditional with polymorphismHighCharacterization tests for every branch, not just the common path (the rare branch is where the bug hides)
Untangle nested callbacksHighA golden-master test capturing exact output and ordering before touching control flow
Break a god classHighestA full call-site map and characterization suite; do this last, after every other net is already in place

What are the best AI prompts for refactoring legacy code, by transformation?

Each prompt below names one transformation. Fill in the brackets, paste only the code in question, and always ask for a diff. If you're building a broader personal toolkit around this, treat these as reusable assets worth versioning like code rather than retyping from scratch each time.

1–3. Extract function

Use this when a function does more than one job, or when a block of logic inside it deserves its own name and its own test.

Extract lines [START]–[END] of the function below into a new function named
[NAME]. Do not change any logic, variable names, or formatting outside that
block. The extracted function must take the same inputs and produce the same
output as the inline block did. Show the result as a diff, not a full rewrite.

[PASTE FUNCTION]
Identify any block inside this function that could be extracted into a
named helper without changing behavior. List each candidate block with a
one-line reason. Do not perform the extraction yet, just propose candidates
so I can pick one.

[PASTE FUNCTION]
Write a characterization test for the function below before I extract
anything from it. Cover at least 3 typical inputs and 2 edge cases, asserting
only what the function currently returns, not what you think it should
return.

[PASTE FUNCTION]

4–6. Rename for intent

Use this to replace a vague or misleading name with one that describes what the code actually does. It's the lowest-risk transformation on this list, and a good first move on any unfamiliar file.

Suggest a better name for [variable/function/class] called `[CURRENT_NAME]`
based on what it actually does in the code below, not what its current name
implies. Give 3 alternatives with a one-sentence reason for each. Do not
rename it yet.

[PASTE CODE]
Rename `[CURRENT_NAME]` to `[NEW_NAME]` everywhere it's declared and called
in the file below. List every call site you changed so I can verify none
were missed. Do not change anything else.

[PASTE FILE]
This function's name suggests it only reads data, but review the body below
and tell me if it also has side effects (writes, mutations, network calls).
If it does, propose a name that makes that honest.

[PASTE FUNCTION]

7–9. Collapse duplication

Use this when the same logic appears in two or more places and has drifted slightly, or hasn't drifted yet but will.

Compare these two functions and tell me precisely where they differ in
behavior, not just formatting. List every input where they would produce a
different output.

[PASTE FUNCTION A]
[PASTE FUNCTION B]
These two functions are near-duplicates. Propose a single shared function
that preserves the exact behavior of both, using a parameter to handle their
one real difference. Do not merge them yet, show me the proposed signature
and how each call site would need to change.

[PASTE FUNCTION A]
[PASTE FUNCTION B]
Write characterization tests that cover the current behavior of both
functions below, including the case where they diverge, before I collapse
them into one.

[PASTE FUNCTION A]
[PASTE FUNCTION B]

10–11. Replace conditional with polymorphism

Use this on a long if/else or switch chain that branches on a type or category, where each branch will keep growing independently. Martin Fowler's catalog name for this exact transformation is Replace Conditional with Polymorphism (accessed August 2026). Treat it as high-risk: every branch is a distinct behavior to preserve.

List every branch of the conditional below, what type or category triggers
it, and what it returns or does. Do not restructure anything yet, I need
the full branch map to write tests against first.

[PASTE CONDITIONAL BLOCK]
Propose a class-per-branch design that replaces this conditional with
polymorphism, matching the branch map below exactly. Show only the proposed
class shapes and method signatures, no implementation yet, so I can
confirm every branch is accounted for before you write the code.

[PASTE CONDITIONAL BLOCK]
[PASTE BRANCH MAP FROM PREVIOUS STEP]

12–14. Untangle nested callbacks

Use this on deeply nested callback pyramids where execution order and error handling are easy to get wrong when converting to promises or async/await.

Trace the execution order of the nested callbacks below, including every
error path. Write it out as a numbered sequence before proposing any
change.

[PASTE CALLBACK CODE]
Capture the current output and side-effect order of this code as a
golden-master test. Run it with [SAMPLE INPUT] and record the exact
output and the order operations occur in, so I can diff future versions
against it.

[PASTE CALLBACK CODE]
Convert the nested callbacks below to async/await, preserving the exact
execution order and error-handling behavior from the sequence I traced
earlier. Flag anywhere the conversion could change timing or ordering,
even subtly, rather than silently "fixing" it.

[PASTE CALLBACK CODE]
[PASTE EXECUTION ORDER FROM PREVIOUS STEP]

15–17. Break a god class

Use this last, on a class that has taken on too many responsibilities over years of "just add it here." This is the highest-risk transformation on the list. Sequence it after every other safety net is in place.

List every public method on the class below, every private field it
touches, and group them by which responsibility they belong to. Do not
propose a split yet, I need the responsibility map first.

[PASTE CLASS]
Find every place in the codebase that instantiates this class or calls its
public methods. List each call site with the file and line, so I have a
complete map before extracting anything.

[PASTE CLASS]
[PASTE SEARCH RESULTS OR RELEVANT FILES]
Using the responsibility map and call-site map from the previous two steps,
propose extracting [RESPONSIBILITY NAME] into its own class. Show only the
proposed new class's public interface and how the original class would
delegate to it. No full implementation yet.

[PASTE CLASS]
[PASTE MAPS FROM PREVIOUS STEPS]

18–20. Introduce a seam for testing

Use this when code can't be tested at all in its current form, usually because it reaches out to a database, the filesystem, or the network directly inside the logic you want to change. A "seam," in Feathers' terms, is a place you can alter behavior without editing the code in that place, most often by extracting an interface the real dependency and a test double can both satisfy.

This function calls [DEPENDENCY, e.g. the database/filesystem/network]
directly, which makes it impossible to test in isolation. Propose a seam,
an interface or parameter, that would let a test substitute a fake for
that dependency, without changing what the function does today.

[PASTE FUNCTION]
Extract the direct call to [DEPENDENCY] into its own method, and show how I
would inject a fake version of it for testing. The function's real behavior
against the real dependency must stay exactly the same.

[PASTE FUNCTION]
Now that [DEPENDENCY] is injectable, write a test for this function using a
fake in place of the real dependency, asserting the same behavior the
function has today.

[PASTE FUNCTION WITH SEAM]

21–23. Modernize idioms

Use this to bring old syntax up to date: var to const/let, string concatenation to template literals, callback style to promises, without touching the logic underneath.

Convert every `var` declaration in the code below to `const` or `let`,
choosing correctly based on whether each variable is reassigned. Do not
change any logic, only the declaration keyword.

[PASTE CODE]
Rewrite the string concatenation below using template literals. Confirm
the resulting string is character-for-character identical to the original
for at least 3 sample inputs.

[PASTE CODE]
This function uses an old idiom: [DESCRIBE, e.g. `.then()` chains,
`arguments` object, `for` loop over `.map()`]. Modernize only that idiom.
Do not change variable names, control flow, or anything not directly part
of the idiom itself.

[PASTE CODE]

24–25. Tighten types

Use this last of all, once the code underneath has already been extracted, renamed, and de-duplicated. Adding or narrowing types on tangled code just relocates the confusion into the type system.

Infer the narrowest correct type for every parameter and return value in
the function below, based on how it's actually called across the codebase
(paste call sites if you have them). Flag any place the current usage is
inconsistent, rather than picking a type that silently papers over it.

[PASTE FUNCTION]
[PASTE CALL SITES IF AVAILABLE]
Add the types proposed above without changing any runtime logic. Run
against every characterization test we wrote earlier and confirm nothing
that passed before now fails only because of a stricter type.

[PASTE FUNCTION WITH PROPOSED TYPES]

How do you know a refactor is safe to ship?

Every characterization test that passed before your change still passes after it, you changed exactly one named transformation, and you can explain the diff in one sentence without saying "and also." If any of those three fail, you didn't refactor. You rewrote, and the fact that it compiles doesn't tell you what changed.

That discipline is slower than pasting a file and accepting whatever comes back. On code with no tests, no docs, and no one left who remembers why a branch exists, slower is the only version of this that doesn't eventually break something in production. For prompts that go the other direction, spotting problems before you touch anything, 35 AI prompts for code review, debugging, and refactoring and the broader developer prompt set are worth having alongside this list.

Free Chrome Extension

Stop rewriting prompts. Start shipping.

Works with ChatGPT, Claude, Gemini, Grok, Midjourney, Ideogram, Veo3 & Kling. 5.0★ on the Chrome Web Store.

Create An Account

Frequently asked questions

Free Chrome Extension

Stop rewriting prompts. Start shipping.

Works with ChatGPT, Claude, Gemini, Grok, Midjourney, Ideogram, Veo3 & Kling. 5.0★ on the Chrome Web Store.

Create An Account