Skip to content
Lesson 5 of 12

Prompting for Refactoring

8 min read

The Refactoring Challenge

Refactoring is one of the highest-risk tasks you can give an AI tool. Code generation starts from nothing -- if the output is wrong, you just delete it. Refactoring modifies existing, working code. If the output is wrong, you might not notice until something breaks in production. This makes the precision of your refactoring prompts critical.

The core principle of a good refactoring prompt is: current state + target state + constraints. The AI needs to know exactly what exists, exactly what you want it to become, and exactly what should NOT change in the process.

The Refactoring Prompt Structure

Every refactoring prompt should answer three questions:

  1. What is the current state? -- What code exists and how does it work now?
  2. What is the target state? -- What should it look like after refactoring?
  3. What are the constraints? -- What must remain unchanged?
Current state: The UserService class in src/services/UserService.ts has a
200-line createUser() method that handles validation, password hashing,
database creation, email sending, and audit logging all in one function.

Target state: Split createUser() into smaller, single-responsibility methods:
validateUserInput(), hashPassword(), persistUser(), sendWelcomeEmail(), and
logUserCreation(). The createUser() method should orchestrate these smaller
methods.

Constraints:
- The public API must not change (createUser still takes the same params and
  returns the same type)
- All existing tests in UserService.test.ts must continue to pass
- Do not change the database schema or Prisma queries
- Keep all methods in the same class for now

Extracting Functions

Extracting large functions into smaller ones is one of the most common refactoring tasks. The key is specifying the extraction boundaries.

Bad:

Break this big function into smaller functions

This gives the AI no guidance on where to make the cuts. It might split along arbitrary boundaries that do not correspond to logical units.

Good:

Extract the following logic from processOrder() in src/services/OrderService.ts
into separate private methods:

1. Lines 15-30: inventory validation -> validateInventory(items: OrderItem[]): Promise<void>
2. Lines 32-55: price calculation including discounts -> calculateTotal(items: OrderItem[], coupon?: Coupon): number
3. Lines 57-78: payment processing -> processPayment(total: number, paymentMethod: PaymentMethod): Promise<string>
4. Lines 80-95: order record creation -> createOrderRecord(customerId: string, items: OrderItem[], transactionId: string): Promise<Order>

processOrder() should call these methods in sequence. If any step fails, the
error should propagate up without additional wrapping.

By specifying exact line ranges, function signatures, and naming, you eliminate ambiguity. The AI will produce exactly the refactoring you want.

Converting Patterns

Pattern conversion is another common refactoring task: converting class components to functional components, callbacks to promises, CommonJS to ES modules, or raw SQL to ORM queries.

Class to Functional Component

Convert the UserProfile class component in src/components/UserProfile.tsx to
a functional component with hooks.

Conversions:
- this.state -> useState hooks (separate hook for each state field)
- componentDidMount -> useEffect with empty dependency array
- componentDidUpdate for userData -> useEffect with [userId] dependency
- this.handleSubmit -> const handleSubmit with useCallback
- Class instance variable this.abortController -> useRef

Maintain all existing behavior:
- Data fetching on mount and when userId changes
- Abort controller cleanup on unmount
- Form submission with optimistic UI update
- Error boundary integration (keep the ErrorBoundary wrapper)

Do NOT change the props interface or the rendered JSX structure.

Callback to Async/Await

Refactor the file upload pipeline in src/utils/fileUpload.ts from nested
callbacks to async/await.

Current pattern:
validateFile(file, (err, validFile) => {
  compressImage(validFile, (err, compressed) => {
    uploadToS3(compressed, (err, url) => {
      updateDatabase(url, (err, record) => { ... })
    })
  })
})

Target pattern:
async function uploadFile(file: File): Promise<UploadRecord> {
  const validFile = await validateFile(file);
  const compressed = await compressImage(validFile);
  const url = await uploadToS3(compressed);
  const record = await updateDatabase(url);
  return record;
}

Each callback-based function should be wrapped in a promisified version.
Keep the original callback functions intact and create new async wrappers.
Add proper error handling with try/catch that preserves the error types from
each step.

Specifying What Should NOT Change

Constraints are the most important part of refactoring prompts. Without them, the AI may "improve" things you did not ask it to touch, introducing subtle bugs.

Refactor the data fetching layer in src/api/client.ts to use axios instead
of the native fetch API.

Change:
- All fetch() calls -> axios equivalents
- Response parsing (no more .json() calls)
- Error handling to use axios error types

Do NOT change:
- The exported function signatures (getUser, createUser, etc.)
- The retry logic in withRetry()
- The authentication header injection in getAuthHeaders()
- The request/response logging interceptor
- The base URL configuration from environment variables
- Any test files

The "Do NOT change" list is as important as the "Change" list. It protects the parts of the code that work correctly and should not be modified.

The Refactor-in-Steps Technique

For large refactoring tasks, break them into sequential, reviewable steps. This is critical because each step can be verified before proceeding to the next one.

I need to migrate our state management from Redux to Zustand. Let's do this
in steps. For Step 1, focus only on the user store.

Step 1: Convert src/store/userSlice.ts from a Redux slice to a Zustand store.

Current Redux slice exports: selectUser, selectIsAuthenticated, setUser,
clearUser, updateProfile.

Create src/store/useUserStore.ts as a Zustand store with the same state shape
and actions. Export a hook useUserStore that provides the same selectors and
actions.

Do not modify any components yet. Do not touch other Redux slices. Just create
the new Zustand store file that mirrors the existing Redux functionality.

I will update the components in Step 2 after reviewing this store.

After reviewing and approving Step 1:

Step 2: Update components that use the user Redux slice to use the new
Zustand store.

Files to update:
- src/components/Header.tsx (uses selectUser and clearUser)
- src/components/ProfileForm.tsx (uses selectUser and updateProfile)
- src/pages/Login.tsx (uses setUser)
- src/pages/Settings.tsx (uses selectUser, updateProfile)

For each file:
- Replace useSelector(selectUser) with useUserStore(state => state.user)
- Replace useDispatch() + dispatch(setUser(data)) with useUserStore.getState().setUser(data)
- Remove Redux imports

Do not remove the old Redux slice yet (other slices may depend on the store).

This step-by-step approach lets you verify each change before building on it. If Step 1 has an issue, you fix it before Step 2 builds on a broken foundation.

Testing Requirements in Refactoring Prompts

Always specify testing expectations in refactoring prompts. The whole point of refactoring is changing the internal structure without changing the external behavior -- and tests verify that.

Refactor the OrderService to use the Repository pattern. Extract all Prisma
queries into a new OrderRepository class in src/repositories/OrderRepository.ts.

Testing requirements:
- All existing tests in src/services/OrderService.test.ts must pass without
  modification
- Create src/repositories/OrderRepository.test.ts with unit tests for each
  repository method
- The OrderService should receive the repository via constructor injection
  so it can be mocked in tests

Migration Prompts

Migrations are large-scale refactoring tasks that change libraries, frameworks, or fundamental patterns. They require especially careful prompting.

Library Migration

Migrate the date handling in src/utils/dates.ts from moment.js to date-fns.

The file exports 8 functions. Migrate them one by one:
1. formatDate(date, format) - use date-fns format()
2. parseDate(dateString) - use date-fns parseISO()
3. addDays(date, days) - use date-fns addDays()
4. diffInDays(date1, date2) - use date-fns differenceInCalendarDays()
5. isAfter(date1, date2) - use date-fns isAfter()
6. startOfDay(date) - use date-fns startOfDay()
7. endOfDay(date) - use date-fns endOfDay()
8. formatRelative(date) - use date-fns formatDistanceToNow()

Important: moment format strings differ from date-fns. Convert all format
strings: 'YYYY' -> 'yyyy', 'DD' -> 'dd', 'MM' stays 'MM'.

After migration, moment should not be imported anywhere in this file. Run
the existing tests to verify all date operations produce the same results.

Framework Upgrade

Update our Next.js 13 page src/app/products/page.tsx to Next.js 14 patterns.

Changes needed:
- Replace the getServerSideProps export with a server component that fetches
  data directly
- Move client-side interactivity (search filter, sort buttons) into a
  separate 'use client' component
- Replace the next/router imports with next/navigation
- Update the metadata export to use the new generateMetadata function

Constraints:
- Keep the same URL structure (/products with ?search and ?sort query params)
- Keep the same visual layout and Tailwind classes
- The search and sort functionality must work identically

Reviewing AI Refactoring Output

After the AI produces refactored code, verify three things:

  1. Behavior preservation: Does the code still do the same thing? Run your tests.
  2. Scope adherence: Did the AI only change what you asked it to change? Check the diff carefully.
  3. Pattern consistency: Does the refactored code match the patterns used elsewhere in your codebase?

If any of these checks fail, give specific feedback in your next prompt rather than asking for a complete redo. "The refactored calculateTotal() does not account for the tax-exempt flag that was handled on line 45 of the original function" is much more useful than "this is wrong, try again."