Back to Blog
9 min readTutorial

Using Claude for Code Refactoring: From Legacy to Clean Code

Transform messy legacy code into clean, maintainable code with AI assistance. Practical refactoring strategies using Claude

Using Claude for Code Refactoring: From Legacy to Clean Code

Legacy code can be daunting—sprawling functions, unclear naming, no tests. But with Claude as your refactoring partner, you can systematically transform messy code into clean, maintainable code. Here's how.

The Refactoring Workflow

Step 1: Understand Before Changing

Never refactor code you don't understand.

You: "Can you explain what this function does?
[paste 100-line function]"

Claude: [Provides explanation of purpose, inputs, outputs, side effects]

You: "What are the main problems with this code?"

Claude: [Identifies issues: complexity, coupling, lack of error handling, etc.]

Step 2: Add Tests First

Make refactoring safe by adding tests before changing anything.

You: "This function has no tests. Can you:
1. Analyze what it should do
2. Identify edge cases
3. Write comprehensive tests

[paste function]"

Claude: [Creates test suite covering all scenarios]

Step 3: Refactor Incrementally

Small, safe changes that keep tests passing.

You: "Now refactor this function:
1. Extract helper functions
2. Improve naming
3. Add error handling
4. Add TypeScript types
Make sure all tests still pass"

Claude: [Refactors step by step]

Common Refactoring Patterns

Pattern 1: Extract Function

// ❌ Before: God function that does everything
async function handleCheckout(cart, user, paymentInfo) {
  // Validate cart (20 lines)
  if (!cart || cart.items.length === 0) {
    throw new Error('Cart is empty');
  }
  for (const item of cart.items) {
    if (!item.id || !item.quantity || item.quantity <= 0) {
      throw new Error('Invalid item in cart');
    }
  }

  // Calculate total (30 lines)
  let subtotal = 0;
  for (const item of cart.items) {
    const product = await db.products.findById(item.id);
    subtotal += product.price * item.quantity;
  }
  const tax = subtotal * 0.08;
  const shipping = subtotal > 50 ? 0 : 5.99;
  const total = subtotal + tax + shipping;

  // Process payment (40 lines)
  const paymentIntent = await stripe.paymentIntents.create({
    amount: total * 100,
    currency: 'usd',
    payment_method: paymentInfo.methodId,
  });

  // Create order (25 lines)
  const order = await db.orders.create({
    userId: user.id,
    items: cart.items,
    subtotal,
    tax,
    shipping,
    total,
    paymentIntentId: paymentIntent.id,
  });

  // Send emails (20 lines)
  await sendOrderConfirmation(user.email, order);
  await sendOrderNotificationToAdmin(order);

  return order;
}

// ✅ After: Claude refactors into focused functions
async function handleCheckout(cart, user, paymentInfo) {
  const validatedCart = await validateCart(cart);
  const pricing = await calculatePricing(validatedCart);
  const payment = await processPayment(pricing.total, paymentInfo);
  const order = await createOrder(user, validatedCart, pricing, payment);
  await sendOrderNotifications(user, order);
  return order;
}

async function validateCart(cart) {
  if (!cart?.items?.length) {
    throw new ValidationError('Cart is empty');
  }

  return {
    ...cart,
    items: cart.items.map(validateCartItem),
  };
}

async function calculatePricing(cart) {
  const subtotal = await calculateSubtotal(cart.items);
  const tax = calculateTax(subtotal);
  const shipping = calculateShipping(subtotal);
  const total = subtotal + tax + shipping;

  return { subtotal, tax, shipping, total };
}

async function processPayment(amount, paymentInfo) {
  return await stripe.paymentIntents.create({
    amount: Math.round(amount * 100),
    currency: 'usd',
    payment_method: paymentInfo.methodId,
  });
}

async function createOrder(user, cart, pricing, payment) {
  return await db.orders.create({
    userId: user.id,
    items: cart.items,
    ...pricing,
    paymentIntentId: payment.id,
    status: 'pending',
  });
}

async function sendOrderNotifications(user, order) {
  await Promise.all([
    sendOrderConfirmation(user.email, order),
    sendOrderNotificationToAdmin(order),
  ]);
}

Pattern 2: Replace Conditionals with Polymorphism

// ❌ Before: Long switch statement
function calculateShipping(order) {
  switch (order.shippingMethod) {
    case 'standard':
      if (order.total > 50) return 0;
      return 5.99;
    case 'express':
      if (order.total > 100) return 5.99;
      return 12.99;
    case 'overnight':
      return 24.99;
    case 'international':
      const weight = calculateWeight(order.items);
      return weight * 3.50 + 15.00;
    default:
      throw new Error('Unknown shipping method');
  }
}

// ✅ After: Strategy pattern
const shippingStrategies = {
  standard: (order) => order.total > 50 ? 0 : 5.99,
  express: (order) => order.total > 100 ? 5.99 : 12.99,
  overnight: () => 24.99,
  international: (order) => {
    const weight = calculateWeight(order.items);
    return weight * 3.50 + 15.00;
  },
};

function calculateShipping(order) {
  const strategy = shippingStrategies[order.shippingMethod];
  if (!strategy) {
    throw new Error(`Unknown shipping method: ${order.shippingMethod}`);
  }
  return strategy(order);
}

Pattern 3: Introduce Parameter Object

// ❌ Before: Too many parameters
function createUser(
  firstName,
  lastName,
  email,
  phone,
  address,
  city,
  state,
  zip,
  country,
  preferences
) {
  // ...
}

// ✅ After: Use objects
function createUser(personalInfo, address, preferences) {
  // ...
}

// Even better with TypeScript
interface PersonalInfo {
  firstName: string;
  lastName: string;
  email: string;
  phone: string;
}

interface Address {
  street: string;
  city: string;
  state: string;
  zip: string;
  country: string;
}

interface UserPreferences {
  newsletter: boolean;
  notifications: boolean;
}

function createUser(
  personalInfo: PersonalInfo,
  address: Address,
  preferences: UserPreferences
): Promise<User> {
  // Type-safe and clear
}

Real-World Refactoring Example

Before: Callback Hell

function loadUserDashboard(userId, callback) {
  getUserData(userId, function(err, user) {
    if (err) return callback(err);

    getOrders(userId, function(err, orders) {
      if (err) return callback(err);

      getRecommendations(userId, function(err, recommendations) {
        if (err) return callback(err);

        getNotifications(userId, function(err, notifications) {
          if (err) return callback(err);

          callback(null, {
            user,
            orders,
            recommendations,
            notifications
          });
        });
      });
    });
  });
}

After: Modern Async/Await

async function loadUserDashboard(userId) {
  try {
    const [user, orders, recommendations, notifications] = await Promise.all([
      getUserData(userId),
      getOrders(userId),
      getRecommendations(userId),
      getNotifications(userId),
    ]);

    return {
      user,
      orders,
      recommendations,
      notifications,
    };
  } catch (error) {
    logger.error('Failed to load dashboard', { userId, error });
    throw new DashboardLoadError('Failed to load user dashboard', { cause: error });
  }
}

Refactoring with Claude: Step by Step

Example: Refactoring a Form Component

Initial messy component:

function UserForm() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [nameError, setNameError] = useState('');
  const [emailError, setEmailError] = useState('');
  const [loading, setLoading] = useState(false);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setNameError('');
    setEmailError('');

    if (!name) setNameError('Name required');
    if (!email) setEmailError('Email required');
    if (!name || !email) return;

    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
      setEmailError('Invalid email');
      return;
    }

    setLoading(true);
    try {
      const response = await fetch('/api/users', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ name, email }),
      });

      if (!response.ok) {
        throw new Error('Failed to create user');
      }

      alert('User created!');
      setName('');
      setEmail('');
    } catch (error) {
      alert('Error: ' + error.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input value={name} onChange={(e) => setName(e.target.value)} />
        {nameError && <span>{nameError}</span>}
      </div>
      <div>
        <input value={email} onChange={(e) => setEmail(e.target.value)} />
        {emailError && <span>{emailError}</span>}
      </div>
      <button disabled={loading}>
        {loading ? 'Creating...' : 'Create User'}
      </button>
    </form>
  );
}

Prompt to Claude:

"Refactor this form component to:
1. Extract validation logic
2. Create reusable form hooks
3. Improve error handling
4. Add proper TypeScript types
5. Extract API call logic
6. Make it more maintainable"

Claude's refactored version:

// hooks/useForm.ts
interface UseFormOptions<T> {
  initialValues: T;
  validate: (values: T) => Partial<Record<keyof T, string>>;
  onSubmit: (values: T) => Promise<void>;
}

function useForm<T extends Record<string, any>>({
  initialValues,
  validate,
  onSubmit,
}: UseFormOptions<T>) {
  const [values, setValues] = useState<T>(initialValues);
  const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleChange = (field: keyof T) => (
    e: React.ChangeEvent<HTMLInputElement>
  ) => {
    setValues(prev => ({ ...prev, [field]: e.target.value }));
    // Clear error when user types
    if (errors[field]) {
      setErrors(prev => ({ ...prev, [field]: undefined }));
    }
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    const validationErrors = validate(values);
    if (Object.keys(validationErrors).length > 0) {
      setErrors(validationErrors);
      return;
    }

    setIsSubmitting(true);
    try {
      await onSubmit(values);
      setValues(initialValues); // Reset form
    } catch (error) {
      // Handle error (could set general form error)
      console.error('Form submission error:', error);
    } finally {
      setIsSubmitting(false);
    }
  };

  return {
    values,
    errors,
    isSubmitting,
    handleChange,
    handleSubmit,
  };
}

// validation/userValidation.ts
interface UserFormData {
  name: string;
  email: string;
}

export function validateUserForm(
  values: UserFormData
): Partial<Record<keyof UserFormData, string>> {
  const errors: Partial<Record<keyof UserFormData, string>> = {};

  if (!values.name.trim()) {
    errors.name = 'Name is required';
  }

  if (!values.email.trim()) {
    errors.email = 'Email is required';
  } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) {
    errors.email = 'Invalid email address';
  }

  return errors;
}

// api/users.ts
export async function createUser(userData: UserFormData): Promise<User> {
  const response = await fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(userData),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || 'Failed to create user');
  }

  return response.json();
}

// components/UserForm.tsx
interface UserFormData {
  name: string;
  email: string;
}

export function UserForm() {
  const { values, errors, isSubmitting, handleChange, handleSubmit } = useForm<UserFormData>({
    initialValues: { name: '', email: '' },
    validate: validateUserForm,
    onSubmit: async (values) => {
      await createUser(values);
      toast.success('User created successfully!');
    },
  });

  return (
    <form onSubmit={handleSubmit}>
      <FormField
        label="Name"
        value={values.name}
        onChange={handleChange('name')}
        error={errors.name}
      />

      <FormField
        label="Email"
        type="email"
        value={values.email}
        onChange={handleChange('email')}
        error={errors.email}
      />

      <Button type="submit" loading={isSubmitting}>
        Create User
      </Button>
    </form>
  );
}

// components/FormField.tsx
interface FormFieldProps {
  label: string;
  value: string;
  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
  error?: string;
  type?: string;
}

export function FormField({ label, value, onChange, error, type = 'text' }: FormFieldProps) {
  const id = `field-${label.toLowerCase().replace(/\s+/g, '-')}`;

  return (
    <div className="form-field">
      <label htmlFor={id}>{label}</label>
      <input
        id={id}
        type={type}
        value={value}
        onChange={onChange}
        aria-invalid={!!error}
        aria-describedby={error ? `${id}-error` : undefined}
      />
      {error && (
        <span id={`${id}-error`} className="error" role="alert">
          {error}
        </span>
      )}
    </div>
  );
}

Refactoring Checklist

When refactoring with Claude:

  • Understand the code first
  • Write tests before changing
  • Commit before refactoring
  • Make one change at a time
  • Keep tests passing
  • Improve naming
  • Extract functions
  • Remove duplication
  • Add type safety
  • Update documentation
  • Review for security issues
  • Commit after successful refactor

Red Flags to Watch For

Even after Claude's refactoring, watch for:

  1. Over-abstraction - Don't create abstractions for 1-2 uses
  2. Premature optimization - Keep it simple first
  3. Breaking changes - Ensure backward compatibility
  4. Lost error handling - Don't remove important checks
  5. Performance regressions - Profile before and after

Conclusion

Refactoring with Claude accelerates the transformation of legacy code:

  1. Start with understanding - Let Claude explain complex code
  2. Add tests first - Make refactoring safe
  3. Refactor incrementally - Small, safe steps
  4. Extract and simplify - Break down complexity
  5. Add types - Catch errors at compile time
  6. Always review - Claude suggests, you decide

The goal isn't perfect code—it's code that's easier to understand, maintain, and modify. Use Claude to get there faster, but always apply your judgment and domain expertise.

Clean code is a journey, not a destination. Start refactoring today!

👨‍💻

Jordan Patel

Web Developer & Technology Enthusiast