Intermediate20 min

Learn prompting techniques to effectively guide AI in refactoring code—improving structure, readability, and maintainability.

Prompt Engineering for Refactoring

Refactoring transforms working code into better code. AI can be a powerful refactoring partner when given the right prompts—but without clear direction, it may make changes you don't want.

The Refactoring Mindset

Key principles when refactoring with AI:

  1. Behavior preservation - Code should work exactly the same after refactoring
  2. Incremental changes - Small, verifiable steps are safer than big rewrites
  3. Clear goals - Know what "better" means for your specific situation
  4. Test coverage - Have tests to verify behavior is preserved

Refactoring Prompt Essentials

Always specify:

  • What to refactor (specific code)
  • Why (the problem with current code)
  • Goal (what better looks like)
  • Constraints (what must not change)

Basic Refactoring Template

Terminal
Refactor this code to [specific goal].

Current code:
```[language]
[code to refactor]

Problems with current code:

  • [problem 1]
  • [problem 2]

Refactoring goals:

  • [goal 1]
  • [goal 2]

Constraints:

  • Must maintain the same public API
  • Must remain backward compatible
  • Must not change [specific behavior]

Please:

  1. Show the refactored code
  2. Explain each change
  3. Note any potential risks
Terminal

## Common Refactoring Scenarios

### Extract Function/Component

Extract reusable parts from this component.

Current code:

Terminal
const OrderPage = ({ orderId }) => {
  const [order, setOrder] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchOrder(orderId)
      .then(setOrder)
      .finally(() => setLoading(false));
  }, [orderId]);

  if (loading) return <div className="spinner animate-spin" />;
  if (!order) return <div className="error">Order not found</div>;

  return (
    <div>
      <h1>{order.id}</h1>
      {/* ... more JSX */}
    </div>
  );
};

Extract:

  1. Loading spinner component
  2. Error display component
  3. Data fetching logic into a custom hook

Keep the main component's structure recognizable.

Terminal

### Simplify Conditionals

Simplify these nested conditionals while preserving logic.

Current code:

Terminal
function getShippingCost(order: Order): number {
  if (order.isPremiumMember) {
    if (order.total > 100) {
      return 0;
    } else {
      if (order.items.length > 5) {
        return 5;
      } else {
        return 10;
      }
    }
  } else {
    if (order.total > 200) {
      return 0;
    } else {
      if (order.total > 100) {
        return 10;
      } else {
        return 20;
      }
    }
  }
}

Goals:

  • Reduce nesting
  • Make conditions clearer
  • Consider using early returns
  • Preserve exact same behavior
Terminal

### Improve Naming

Improve variable and function names in this code.

Current code:

Terminal
function proc(d) {
  const r = [];
  for (let i = 0; i < d.length; i++) {
    const x = d[i];
    if (x.t === 'A') {
      const n = x.v * 1.1;
      r.push({ ...x, v: n });
    } else {
      r.push(x);
    }
  }
  return r;
}

Context: This processes a list of transactions, applying a 10% fee to type 'A' transactions.

Requirements:

  • Names should be descriptive
  • Follow JavaScript naming conventions
  • Keep function concise
Terminal

### Remove Code Duplication

Remove duplication from these similar functions.

Current code:

Terminal
async function fetchUsers() {
  try {
    const response = await fetch('/api/users');
    if (!response.ok) throw new Error('Failed to fetch users');
    const data = await response.json();
    return { success: true, data };
  } catch (error) {
    console.error('Error fetching users:', error);
    return { success: false, error: error.message };
  }
}

async function fetchOrders() {
  try {
    const response = await fetch('/api/orders');
    if (!response.ok) throw new Error('Failed to fetch orders');
    const data = await response.json();
    return { success: true, data };
  } catch (error) {
    console.error('Error fetching orders:', error);
    return { success: false, error: error.message };
  }
}

async function fetchProducts() {
  try {
    const response = await fetch('/api/products');
    if (!response.ok) throw new Error('Failed to fetch products');
    const data = await response.json();
    return { success: true, data };
  } catch (error) {
    console.error('Error fetching products:', error);
    return { success: false, error: error.message };
  }
}

Create a reusable abstraction that:

  • Eliminates the duplication
  • Maintains type safety
  • Is flexible enough for different endpoints
  • Handles the same error cases
Terminal

### Convert to TypeScript

Convert this JavaScript code to TypeScript with proper types.

Current JavaScript:

Terminal
function processOrder(order, options = {}) {
  const { applyDiscount = false, notifyUser = true } = options;

  let total = order.items.reduce((sum, item) => sum + item.price * item.qty, 0);

  if (applyDiscount && order.coupon) {
    total = total * (1 - order.coupon.percent / 100);
  }

  return {
    orderId: order.id,
    total,
    itemCount: order.items.length,
    discounted: applyDiscount && !!order.coupon
  };
}

Requirements:

  • Create proper interfaces for Order, Item, Options, etc.
  • Use strict types (no 'any')
  • Add JSDoc comments for complex types
  • Handle nullable fields appropriately
Terminal

### Modernize Legacy Code

Modernize this legacy JavaScript to use modern ES2022+ features.

Legacy code:

Terminal
var UserService = {
  users: [],

  getUser: function(id) {
    var user = null;
    for (var i = 0; i < this.users.length; i++) {
      if (this.users[i].id === id) {
        user = this.users[i];
        break;
      }
    }
    return user;
  },

  addUser: function(user) {
    var self = this;
    return new Promise(function(resolve, reject) {
      setTimeout(function() {
        self.users.push(user);
        resolve(user);
      }, 100);
    });
  },

  filterActiveUsers: function() {
    return this.users.filter(function(user) {
      return user.active === true;
    });
  }
};

Modernize using:

  • const/let instead of var
  • Arrow functions
  • Array methods (find, filter)
  • Async/await
  • Object shorthand
  • Optional chaining where appropriate
Terminal

## Refactoring with Constraints

### Preserve API Compatibility

Refactor this module's internals WITHOUT changing its public API.

Current code:

Terminal
export class UserManager {
  private users: Map<string, User> = new Map();

  addUser(user: User): void {
    // Complex internal logic
  }

  getUser(id: string): User | undefined {
    // Complex internal logic
  }

  removeUser(id: string): boolean {
    // Complex internal logic
  }
}

Constraints:

  • Method signatures must stay identical
  • Return types must stay identical
  • Existing consumers must not break
  • Tests using public API should still pass

Goals:

  • Improve internal efficiency
  • Add better error handling internally
  • Simplify the implementation
Terminal

### Maintain Backward Compatibility

Refactor this function to a better design while maintaining backward compatibility.

Current signature:

Terminal
function formatDate(date: Date, format?: string): string

Desired new design:

Terminal
function formatDate(options: FormatDateOptions): string

Requirements:

  • New code should use the new signature
  • Old code should still work (deprecated but functional)
  • Add deprecation warning for old usage
  • Both should use the same core logic
Terminal

## Step-by-Step Refactoring

For complex refactors, use a staged approach:

Help me refactor this large function in stages.

Current function (150 lines):

Terminal
function processCheckout(cart, user, paymentInfo) {
  // [large complex function]
}

Stage 1: Identify distinct responsibilities

  • List each responsibility this function handles

Stage 2: Plan the extraction

  • Which parts should become separate functions?
  • What should the interfaces look like?

Stage 3: Execute incrementally

  • Refactor one piece at a time
  • Verify behavior after each step

Let's start with Stage 1. Analyze the responsibilities in this function.

Terminal

## Performance-Focused Refactoring

Refactor for better performance.

Current code:

Terminal
function findCommonElements(arr1: number[], arr2: number[]): number[] {
  const result: number[] = [];
  for (const item1 of arr1) {
    for (const item2 of arr2) {
      if (item1 === item2 && !result.includes(item1)) {
        result.push(item1);
      }
    }
  }
  return result;
}

Performance issue: O(n³) complexity due to nested loops and includes check.

Target: Reduce to O(n) or O(n log n).

Constraints:

  • Must return same results (same order not required)
  • Must handle duplicates the same way
  • No external dependencies
Terminal

## Testing Considerations

Refactor this code, ensuring it remains testable.

Current code:

Terminal
async function sendNotification(userId: string, message: string) {
  const user = await db.users.findById(userId);
  const result = await emailService.send(user.email, message);
  await db.notifications.create({ userId, message, sentAt: new Date() });
  return result;
}

Problems:

  • Hard to unit test (depends on db and emailService)
  • No dependency injection
  • Side effects mixed with logic

Refactor to:

  • Allow dependency injection
  • Separate pure logic from side effects
  • Make unit testing easy
Terminal

## When NOT to Refactor with AI

Be cautious about AI refactoring when:

- **No test coverage** - Can't verify behavior preservation
- **Critical production code** - High risk of undetected changes
- **Complex domain logic** - AI may misunderstand business rules
- **Performance-critical paths** - Need measured optimization, not guesses

## Practice Exercise

Create refactoring prompts for these scenarios:

1. **God class** - A 500-line class doing too many things
2. **Callback hell** - Deeply nested callbacks to convert to async/await
3. **Magic numbers** - Code full of unexplained numeric values
4. **Long parameter list** - Function with 10+ parameters

For each, specify:
- The specific problem
- Refactoring goal
- Constraints
- How to verify the refactoring worked

## Summary

- Always specify what to refactor, why, and the goal
- Set clear constraints (API compatibility, behavior preservation)
- Use staged refactoring for complex changes
- Verify with tests after refactoring
- Be explicit about what must NOT change

## Next Steps

Now let's explore context management—how to effectively manage what information you include in prompts to get optimal results.
Mark this lesson as complete to track your progress