Refactoring is where an agentic editor either saves your afternoon or quietly rewrites behavior you didn't want touched. The difference is the prompt. Every prompt below tells Cascade — Windsurf's agent — to preserve behavior, run the existing tests, and stay inside the files you name. They lean on the mechanics that actually make Windsurf reliable: @codebase for cross-file work, explicit acceptance criteria, "run the tests / don't touch unrelated files," per-file diff review, and .windsurf/rules for conventions that outlast a single session.

Paste any prompt into the Cascade pane in Write mode, fill the [bracketed placeholders], and review the diff before accepting. For the full library, start with the 40 Best Windsurf AI Prompts roundup; when a refactor exposes a bug, jump to the debugging prompts.

Advertisement

Safe refactors (keep tests green)

A safe refactor changes structure, not behavior. Give Cascade the file, tell it to run the tests before and after, and — if there are no tests — have it write characterization tests first so any behavior change shows up red.

1. Readability refactor with no behavior change

In Write mode, refactor @file:[path/to/file] for readability ONLY.

Hard rules:
- No behavior change. Keep the public API and return values identical.
- Do not touch any other file.
- Run [test command, e.g. npm test] BEFORE you start and confirm it passes; run it again AFTER and confirm it still passes.

Improve naming, break up long expressions, and add short comments only where intent is unclear. Show me the per-file diff and the final test output.

Why it works: the "no behavior change" rule plus a before/after test run turns the whole change into a verifiable operation instead of a rewrite.

2. Add characterization tests before refactoring

@file:[path/to/untested-file] has no tests. Before we refactor it, write characterization tests that pin its CURRENT observable behavior — including edge cases and error paths — in [test framework].

Do not change the source file yet. Run the new tests and confirm they pass against the existing code. Report anything you couldn't cover.

Best for: legacy code you're afraid to touch. Get to green first, then refactor with a net.

3. Plan a refactor before touching code

In Chat mode (read-only), read @file:[path] and propose a step-by-step refactoring plan to improve [readability / testability / structure].

For each step, state: what changes, why, and the risk of a behavior change. Do NOT edit anything yet. I'll approve the plan, then you execute it one step at a time and run [test command] after each step.

Why it works: Chat mode won't edit, so you get the plan first; executing step by step keeps each diff small and reviewable.

4. Refactor a whole file cleanly

Refactor @file:[path] end to end. Goals, in priority order:
1. No behavior change (verify with [test command]).
2. Consistent naming and formatting with the rest of @folder:[dir].
3. Smaller functions, fewer nesting levels, no dead code.

Constraints: only edit this one file; keep all exports and their signatures. Acceptance: tests pass and the public interface is byte-for-byte the same. Show the diff.

Structure & extraction

Big functions and fat files hide bugs. Ask Cascade to pull cohesive pieces into named units, introduce a boundary, or apply a pattern — while the tests keep proving behavior is unchanged.

5. Extract functions from a god function

The function [functionName] in @file:[path] is too long and does several things. Extract each distinct responsibility into its own well-named helper function in the same file.

Rules: no behavior change; keep [functionName]'s signature and return value identical; the extracted helpers should be pure where possible. Run [test command] after and confirm green. Show me the diff grouped by helper.

Best for: 200-line functions where you can name the steps out loud but the code doesn't.

6. Split a large module into files

@file:[path/to/big-module] has grown too large. Split it into cohesive files under @folder:[target-dir], grouping by responsibility.

Requirements:
- Preserve the public API: re-export everything from an index/barrel so existing imports keep working.
- Use @codebase to update any internal imports that break.
- No behavior change. Run [test command] and confirm all tests pass.
Do not modify files outside [target-dir] except imports that must change. Show the new file list and the diff.

Why it works: the barrel re-export keeps call sites stable, and @codebase catches the imports a plain move would break.

7. Introduce a service-layer boundary

@file:[path/to/handler] mixes HTTP/route logic with business logic and data access. Introduce a clear boundary: keep the handler thin and move business logic into a [service/use-case] module, and data access behind a [repository] interface.

Rules: no behavior change; the route's request/response contract stays identical; add no new dependencies without asking. Run [test command] after. Explain the new layering in 3 bullets and show the diff.

8. Apply a design pattern where it fits

Look at @function:[name] in @file:[path]. It has a growing if/else (or switch) on [some type/flag]. If — and only if — a [Strategy / Factory / State] pattern genuinely simplifies it, refactor to that pattern; otherwise tell me why it isn't worth it and stop.

If you proceed: no behavior change, keep the same inputs/outputs, and run [test command]. Show the diff and a one-line rationale.

Best for: avoiding pattern-for-pattern's-sake — the "only if it genuinely simplifies" clause lets Cascade decline.

Advertisement

Remove cruft

Duplication, dead code, and deep nesting are the easiest wins — and the easiest to get wrong when done blind. Scope each one and require the tests stay green before Cascade deletes or merges anything.

9. Remove duplication (DRY)

Use @codebase to find blocks duplicated between @file:[fileA] and @file:[fileB] (and anywhere else). Extract the shared logic into a single well-named helper in [shared/util path] and update all call sites to use it.

Rules: no behavior change; if the "duplicates" differ in a meaningful way, keep them separate and tell me. Run [test command] and confirm green. Show me each replaced call site in the diff.

Why it works: the escape hatch for "meaningful differences" stops Cascade from collapsing code that only looks identical.

10. Delete dead code safely

I think [functionName / export / file] is unused. Use @codebase to search the entire repo for any references, imports, dynamic usage, or string-based lookups of it.

If there are zero real references, delete it and any now-orphaned imports. If there is ANY reference, do not delete — list where it's used. Run [test command] after. Show the diff and the list of references you found.

Best for: confidently removing code without grep-guessing — @codebase covers dynamic and string references too.

11. Flatten nested conditionals with early returns

Refactor @function:[name] in @file:[path] to reduce nesting. Use guard clauses / early returns to handle edge and error cases up front, so the happy path is flat and unindented.

No behavior change — every branch that returned or threw before must still return or throw the same thing. Run [test command] after and confirm green. Show the diff.

12. Replace magic numbers with named constants

In @file:[path], find magic numbers and hard-coded string literals that carry meaning (timeouts, limits, status codes, keys) and replace them with named constants at the top of the file or in [constants module].

Rules: values must be identical — no behavior change; don't touch literals that are self-explanatory (like 0, 1, or array indices). Run [test command] after. Show the diff.

Why it works: naming the "don't touch 0/1/indices" boundary keeps the change to genuinely meaningful values.

Types & modern syntax

Types and modern APIs make code self-documenting and catch bugs at compile time. Add them incrementally, keep runtime behavior identical, and let the type checker or tests confirm nothing shifted.

13. Add types to untyped JavaScript

Convert @file:[path.js] to TypeScript (or add JSDoc types if we're staying JS). Infer accurate types from usage across @codebase — don't fall back to `any`.

Rules: runtime behavior must not change; keep exports and their names identical; update imports of this file as needed. Run [type-check command, e.g. tsc --noEmit] and [test command] and confirm both pass. Flag anything you genuinely couldn't type and why.

Best for: onboarding a JS file to TypeScript without a big-bang rewrite.

14. Add type hints to untyped Python

Add precise type hints to every function and method in @file:[path.py], inferring types from how callers use them (@codebase) and from return values.

Rules: no runtime behavior change; use standard `typing` constructs; add `from __future__ import annotations` if it helps. Run [mypy / pyright] and [test command] and confirm both are clean. List any signatures you had to leave as loose types.

15. Tighten loose or `any` types

Use @codebase to find `any`, `unknown` used as a cop-out, and overly broad types in @folder:[dir]. Replace them with precise types or discriminated unions based on real usage.

Rules: no behavior change; do not add runtime casts to silence the checker — fix the type instead. Run [type-check command] and [test command]. Show me each tightened type in the diff with a one-line note on what it now guarantees.

Why it works: banning "cast to silence the checker" forces real type fixes, not as any band-aids.

16. Migrate to modern syntax and APIs

Modernize @file:[path] to current [language/framework] idioms: [e.g. var → const/let, promises → async/await, .then chains, deprecated API X → Y]. Use @docs:[library] to confirm the current recommended API.

Rules: no behavior change; don't upgrade dependency versions; keep the public interface identical. Run [test command] after. Show the diff and list any deprecated API you replaced.

17. Clean up async/await and remove callback hell

Refactor @function:[name] in @file:[path] to use async/await consistently. Remove nested callbacks and `.then()` chains, and make sure every awaited call has proper error handling (try/catch or a single rejection path).

Rules: no behavior change, including the order of side effects and the exact errors thrown; no unhandled promise rejections. Run [test command] after and confirm green. Show the diff.

Best for: pyramid-of-doom callbacks — the "order of side effects" clause guards against subtle reordering.

Larger migrations

Cross-file renames, version bumps, and repeated mechanical changes are where @codebase and /workflows earn their keep. Keep them scoped, test-verified, and reviewed diff-by-diff.

18. Rename a symbol across the codebase

Rename [oldName] to [newName] across the whole project. Use @codebase to find the definition and EVERY reference — call sites, imports/exports, type usages, and any string references (config keys, tests, docs).

Rules: no behavior change; update all references so nothing breaks; don't rename unrelated symbols that merely contain the substring. Run [test command] after. Show me a per-file diff so I can review each change before accepting.

Why it works: @codebase is semantic, so it finds string-based references a text find-and-replace would miss — and the substring caveat prevents collateral renames.

19. Migrate a library or framework version

We're upgrading [library] from [vX] to [vY]. Use @docs:[library] for the vY migration guide and @codebase to find every place we use it.

Plan first (Chat mode): list each breaking change that affects us and the file it touches. After I approve, apply the changes in Write mode, one concern at a time, running [test command] after each. Rules: no behavior change beyond what the upgrade requires; explain any unavoidable behavior change. Show diffs per file.

Best for: major version bumps where the changelog matters as much as the code.

20. Convert class components to hooks

Convert the class component in @file:[path] to a function component using hooks. Map lifecycle methods to the right hooks (componentDidMount/Update/Unmount → useEffect), state to useState/useReducer, and keep refs and context wiring intact.

Rules: no behavior change — same rendered output, same props/state contract, same effect timing. Run [test command] after and confirm green. Show the diff and note any effect whose timing you had to reason about carefully.

21. Run a codemod-style change as a workflow

Create a reusable workflow file at .windsurf/workflows/apply-codemod.md that performs this repeated refactor across a folder I specify:

1. Use @codebase to list every file in [folder] matching [pattern].
2. For each file, apply this transform: [describe the exact mechanical change].
3. After each file, run [test command]; if it fails, stop and report that file.
4. Never touch files outside the given folder.

Write the workflow so I can invoke it later with /apply-codemod. Show me the file contents.

Why it works: a /workflow turns a repeated, error-prone edit into a one-command, test-gated recipe you can rerun weekly. See the cheat sheet for more workflow syntax.

Consistency & conventions

Rules make a convention stick. A scoped glob rule in .windsurf/rules applies your naming or error-handling standard to a folder every session, so Cascade stops drifting and you stop repeating yourself.

22. Enforce a naming convention with a glob rule

Create a rule file at .windsurf/rules/[area]-conventions.md with a Glob trigger matching [glob, e.g. src/services/**/*.ts]. The rule should state our conventions:

- Naming: [e.g. camelCase functions, PascalCase types, files kebab-case].
- Error handling: [e.g. throw typed AppError, never bare strings; wrap external calls in try/catch and log with context].

Then apply these conventions across @folder:[dir] now: rename and adjust to match, with no behavior change. Run [test command] after. Show the rule file and the diff.

Best for: making a standard durable — the glob rule keeps Cascade compliant on future edits without re-explaining. Pair it with the workflow above and the full prompt roundup to standardize a whole service.

Frequently Asked Questions

How do I refactor with Windsurf without breaking anything?

Tell Cascade the refactor is behavior-preserving, run the existing tests before and after, and forbid it from changing any unrelated files. If there are no tests, have Cascade write characterization tests first, confirm they pass against the current code, then refactor and re-run them.

Should I add tests before refactoring untested code?

Yes. Ask Cascade to write characterization tests that pin the current observable behavior, run them green against the existing code, and only then start refactoring. The tests become your safety net so any behavior change shows up as a red test.

How do I rename a symbol across the whole codebase in Windsurf?

Use @codebase so Cascade semantically searches every file, then have it rename the definition and all call sites, imports, and references. Ask it to run the tests and show a per-file diff so you can review each change before accepting.

Can Cascade remove duplication and dead code safely?

Yes, if you scope it. Point it at the duplicated blocks with @file or @codebase, ask it to extract a single shared helper, and require that every existing test stays green. For dead code, have it confirm no references exist across the repo before deleting.

How do I make Cascade follow a naming or style convention everywhere?

Write a scoped glob rule in .windsurf/rules that targets the folder, describe the convention, and ask Cascade to apply it. Rules persist across sessions, so Cascade enforces the convention on future edits without you repeating it.

What's the safest way to review a big refactor before accepting it?

Ask Cascade to plan before editing, then review the per-file diff one file at a time in the Cascade pane. Confirm the tests pass, reject anything that touches files outside the stated scope, and accept only the diffs you understand.

Advertisement