Automating Framework Migrations with Claude: Angular to React at Scale

How we used Claude to automate 68% of an Angular-to-React migration across 1,200 components, reducing a 9-month project to 11 weeks.

#claude#code-migration#refactoring#ai
Cover image for the article: Automating Framework Migrations with Claude: Angular to React at Scale

Framework migrations are the projects everyone dreads. They're expensive, risky, and tedious. When our team faced migrating a 1,200-component Angular application to React, the initial estimate was 9 months with 6 engineers. Instead, we built a Claude-powered migration assistant that automated 68% of the conversion and reduced the timeline to 11 weeks with 3 engineers focused on review and edge cases.

This post breaks down the architecture of the migration pipeline, the patterns Claude handled well, where it struggled, and the validation framework that gave us confidence to ship.

The Problem

Our Angular 14 application had grown over 5 years into a monolith with:

  • 1,247 components (templates + TypeScript classes)
  • 342 services with complex dependency injection
  • 89 custom directives
  • NgRx state management (214 actions, 67 reducers, 43 effects)
  • 156 unit test files

Manual migration at our team's velocity would take 9 months. The business needed it done in 3 months for a platform consolidation.

Architecture

The migration pipeline operates in four phases: analysis, transformation, validation, and human review.

Migration Pipeline Architecture

Phase 1: Static Analysis and Dependency Mapping

Before Claude touches any code, we build a complete dependency graph of the Angular application.

import * as ts from 'typescript';
import Anthropic from '@anthropic-ai/sdk';

interface ComponentAnalysis {
  filePath: string;
  componentName: string;
  inputs: { name: string; type: string; required: boolean }[];
  outputs: { name: string; eventType: string }[];
  services: string[];
  templateComplexity: 'simple' | 'moderate' | 'complex';
  stateManagement: 'local' | 'ngrx' | 'service-based';
  dependencies: string[];
}

class AngularAnalyzer {
  private program: ts.Program;

  constructor(tsconfigPath: string) {
    const config = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
    this.program = ts.createProgram(
      config.config.files,
      config.config.compilerOptions
    );
  }

  analyzeComponent(filePath: string): ComponentAnalysis {
    const sourceFile = this.program.getSourceFile(filePath);
    const checker = this.program.getTypeChecker();

    // Extract @Input(), @Output(), constructor injections
    const decorators = this.extractDecorators(sourceFile, checker);
    const injections = this.extractConstructorInjections(sourceFile, checker);

    return {
      filePath,
      componentName: this.getComponentName(sourceFile),
      inputs: decorators.inputs,
      outputs: decorators.outputs,
      services: injections,
      templateComplexity: this.assessTemplateComplexity(filePath),
      stateManagement: this.detectStatePattern(sourceFile),
      dependencies: this.resolveDependencies(sourceFile, checker)
    };
  }

  private assessTemplateComplexity(filePath: string): 'simple' | 'moderate' | 'complex' {
    const templatePath = filePath.replace('.ts', '.html');
    const template = ts.sys.readFile(templatePath) || '';
    const directives = (template.match(/\*ng(If|For|Switch)/g) || []).length;
    const pipes = (template.match(/\|/g) || []).length;
    const nestedComponents = (template.match(/<app-/g) || []).length;

    const score = directives * 2 + pipes + nestedComponents * 3;
    if (score < 5) return 'simple';
    if (score < 15) return 'moderate';
    return 'complex';
  }
}

Phase 2: Claude-Powered Transformation

For each component, we construct a migration prompt with full context — the component source, its template, associated styles, test file, and the interfaces of its dependencies.

import anthropic
import json
from pathlib import Path
from dataclasses import dataclass

@dataclass
class MigrationResult:
    react_component: str
    hooks: list[str]
    test_file: str
    confidence: float
    notes: list[str]

class ClaudeMigrationEngine:
    def __init__(self):
        self.client = anthropic.Anthropic()
        self.migration_rules = self._load_migration_rules()

    def migrate_component(self, analysis: dict, source_files: dict) -> MigrationResult:
        prompt = self._build_migration_prompt(analysis, source_files)

        response = self.client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=8192,
            messages=[{
                "role": "user",
                "content": prompt
            }],
            system="""You are an expert Angular-to-React migration engineer.
            Convert Angular components to modern React with TypeScript.
            Rules:
            - Use functional components with hooks
            - Convert @Input() to props interface
            - Convert @Output() to callback props
            - Convert services to custom hooks or context
            - Convert NgRx to Zustand stores
            - Preserve all existing behavior exactly
            - Include proper TypeScript types
            - Convert Angular template syntax to JSX"""
        )

        return self._parse_migration_response(response.content[0].text)

    def _build_migration_prompt(self, analysis: dict, source_files: dict) -> str:
        return f"""Migrate this Angular component to React.

## Component Analysis
- Name: {analysis['componentName']}
- Complexity: {analysis['templateComplexity']}
- State management: {analysis['stateManagement']}
- Services used: {', '.join(analysis['services'])}

## Angular Component (.ts)
```typescript
{source_files['component']}

Template (.html)

{source_files['template']}

Styles (.scss)

{source_files['styles']}

Existing Tests

{source_files['tests']}

Migration Rules

{self.migration_rules}

Return a JSON response with:

  • react_component: the full React component code

  • hooks: any custom hooks needed (as array of code strings)

  • test_file: converted test file using React Testing Library

  • confidence: 0-1 score of migration accuracy

  • notes: any concerns or manual review items"""

    def _load_migration_rules(self) -> str: rules_path = Path("migration-rules.md") return rules_path.read_text() if rules_path.exists() else ""


### Phase 3: Automated Validation

Every migrated component goes through a multi-layer validation pipeline before human review.

```typescript
interface ValidationResult {
  component: string;
  typeCheck: { passed: boolean; errors: string[] };
  renderTest: { passed: boolean; snapshot_diff: string | null };
  behaviorTest: { passed: boolean; failures: string[] };
  accessibilityAudit: { passed: boolean; violations: string[] };
  overallStatus: 'auto-approved' | 'needs-review' | 'failed';
}

class MigrationValidator {
  async validate(
    originalComponent: string,
    migratedComponent: string,
    testFile: string
  ): Promise<ValidationResult> {
    const [typeCheck, renderTest, behaviorTest, a11y] = await Promise.all([
      this.runTypeCheck(migratedComponent),
      this.compareRenderOutput(originalComponent, migratedComponent),
      this.runBehaviorTests(testFile),
      this.runAccessibilityAudit(migratedComponent)
    ]);

    const overallStatus = this.determineStatus(typeCheck, renderTest, behaviorTest, a11y);

    return {
      component: migratedComponent,
      typeCheck,
      renderTest,
      behaviorTest,
      accessibilityAudit: a11y,
      overallStatus
    };
  }

  private determineStatus(...results: any[]): 'auto-approved' | 'needs-review' | 'failed' {
    const allPassed = results.every(r => r.passed);
    const anyFailed = results.some(r => !r.passed && r.errors?.length > 2);

    if (allPassed) return 'auto-approved';
    if (anyFailed) return 'failed';
    return 'needs-review';
  }
}

Results Breakdown

Of 1,247 components:

CategoryCountPercentage
Auto-approved (no human changes)41233%
Minor edits needed43735%
Significant rework needed28923%
Manual migration required1099%

The "minor edits" category typically needed 5-15 minutes of human review. The components requiring manual migration were mostly those with complex custom directives or heavy DOM manipulation.

Patterns Claude Handled Well

  • Simple presentational components (100% accuracy)
  • Form components with reactive forms to React Hook Form
  • NgRx selectors to Zustand selectors
  • Angular pipes to utility functions
  • Structural directives (*ngIf, *ngFor) to JSX conditionals

Patterns That Needed Human Intervention

  • ViewChild/ContentChild references requiring React refs with imperative handles
  • Complex animation triggers (Angular Animations to Framer Motion)
  • Route guards to React Router loaders
  • Dynamic component loading (ComponentFactoryResolver patterns)

Timeline Comparison

  • Original estimate: 9 months, 6 engineers = 54 engineer-months
  • Actual delivery: 11 weeks, 3 engineers = 8.25 engineer-months
  • Cost savings: ~$460,000 in engineering time
  • Claude API cost: ~$3,200 total for all migrations and iterations

Conclusion

Claude won't eliminate the need for human judgment in framework migrations, but it dramatically shifts the work from writing boilerplate to reviewing and refining. The key insight is that migration is pattern matching at scale — exactly what LLMs excel at. The 68% automation rate was achievable because we invested heavily in the validation pipeline. Without automated validation, we wouldn't have trusted any of the output.

If you're facing a similar migration, start by categorizing your components by complexity, build the validation pipeline first, and let Claude handle the repetitive transformations while your engineers focus on the genuinely hard cases.

Comments

    No comments yet. Be the first to share your thoughts.