Module 5: Framework and Language Migration

Migration Strategy — Plan, Test, Migrate, Validate

Migration Strategy — Plan, Test, Migrate, Validate

Capsule description

Before changing a single line of code, you need a strategy. Migrations that fail don't fail because of technical problems — they fail because of a lack of planning. A migrated endpoint that breaks production isn't a bug; it's a migration without a rollback plan. A framework change that takes 6 months instead of 6 weeks isn't a speed problem; it's a scope problem without checkpoints.

In this capsule you're going to learn the migration cycle that prevents these problems: plan → safety net (tests) → migrate incrementally → validate at each step. It's the same principle as refactoring (tests first), but applied at the framework scale.

Claude Code accelerates each phase of the cycle, but the strategic decision — what to migrate first, what the checkpoints are, what plan B is — is yours.


The Migration Cycle

The 4 steps that repeat

PLAN → TEST → MIGRATE → VALIDATE
  ↑                         |
  └─────────────────────────┘
  (repeat for each component)

PLAN: Decide what to migrate in this iteration, in what order, and what the success criterion is.

TEST: Write tests that capture the current behavior of the component to migrate. If there are no tests, write them before touching anything.

MIGRATE: Convert the component to the new framework. One component at a time.

VALIDATE: Run equivalence tests. Does the new component produce the same results as the old one? If yes, continue. If no, fix or revert.


Phase 1: Assessment — Codebase Inventory

What you need to know before planning

# Prompt to Claude Code:
> "Analyze this Flask application and produce a migration
   inventory:
   1. List all the endpoints (method, path, handler)
   2. List all the dependencies (pip packages)
   3. List the middleware/decorators used
   4. Identify Flask-specific patterns (Blueprint,
      g object, before_request, etc.)
   5. Evaluate the existing test coverage
   6. Identify the most and least complex endpoints"

# Expected output:
# Endpoints: 12 (4 GET, 6 POST, 2 PUT)
# Dependencies: flask, flask-login, flask-cors, sqlalchemy, ...
# Middleware: auth_required (custom), cors, logging
# Flask patterns: Blueprint (3), g.user, before_request
# Tests: 8 tests (67% endpoint coverage)
# Most complex: POST /orders (validation + calculation + payment)
# Least complex: GET /health (returns status)

Classify components by complexity

ComponentComplexityFastAPI equivalentSuggested order
GET /healthLowDirect1
GET /usersLowDirect2
POST /usersMediumPydantic validation3
Auth middlewareMediumDepends()4
POST /ordersHighPydantic + Depends5
BlueprintsMediumAPIRouter6

Rule: migrate from lowest to highest complexity. The first endpoints give you confidence and momentum. The complex ones last, when you already master the new framework.


Phase 2: Migration Plan

Structure of the plan

# Migration Plan: Flask → FastAPI

## Scope
- Migrate 12 endpoints from Flask to FastAPI
- Migrate 3 Blueprints to APIRouter
- Migrate custom middleware to Depends()
- Keep SQLAlchemy (doesn't change)

## Phases

### Phase 1: Foundation (Day 1)
- Set up FastAPI project alongside Flask
- Migrate GET /health (smoke test)
- Checkpoint: FastAPI app runs, /health responds

### Phase 2: Simple Endpoints (Day 2-3)
- Migrate 4 GET endpoints
- Migrate 2 simple POST endpoints
- Checkpoint: 6/12 endpoints migrated, tests green

### Phase 3: Complex Endpoints (Day 4-5)
- Migrate auth middleware to Depends()
- Migrate POST /orders (most complex)
- Migrate remaining endpoints
- Checkpoint: 12/12 endpoints migrated, tests green

### Phase 4: Cleanup (Day 6)
- Remove Flask code
- Update dependencies
- Final test suite run
- Checkpoint: FastAPI only, all tests pass

## Rollback Strategy
- Each phase has a rollback to the previous phase
- Git tags at each checkpoint
- If Phase 3 fails: revert to Phase 2, investigate
- Worst case: revert to pre-migration (Flask functional)

## Dependencies to Change
- flask → fastapi + uvicorn
- flask-cors → fastapi-cors (middleware)
- flask-login → custom JWT with python-jose
- Keep: sqlalchemy, pytest, etc.

Generating the plan with Claude Code

# Prompt:
> "Based on the inventory of this Flask app, generate
   a migration plan to FastAPI. Organize it into phases of
   increasing complexity. Each phase has a checkpoint,
   a success criterion, and a rollback strategy. Include
   a Flask→FastAPI dependency mapping."

Phase 3: Safety Net — Tests Before Migrating

Equivalence tests

The central question of any migration: "Does the new code do exactly the same as the old one?"

# tests/test_migration_equivalence.py
import pytest
from flask_app import app as flask_app
from fastapi_app import app as fastapi_app
from fastapi.testclient import TestClient
from flask.testing import FlaskClient

flask_client = flask_app.test_client()
fastapi_client = TestClient(fastapi_app)

class TestMigrationEquivalence:
    """Verifies that Flask and FastAPI produce the same responses."""
    
    def test_get_health_equivalence(self):
        flask_response = flask_client.get("/health")
        fastapi_response = fastapi_client.get("/health")
        
        assert flask_response.status_code == fastapi_response.status_code
        assert flask_response.json == fastapi_response.json()
    
    def test_create_user_equivalence(self):
        data = {"name": "Ana", "email": "ana@test.com"}
        
        flask_response = flask_client.post("/users", json=data)
        fastapi_response = fastapi_client.post("/users", json=data)
        
        assert flask_response.status_code == fastapi_response.status_code
        # Compare structure, not IDs (they can differ)
        flask_body = flask_response.json
        fastapi_body = fastapi_response.json()
        assert flask_body["name"] == fastapi_body["name"]
        assert flask_body["email"] == fastapi_body["email"]
    
    def test_invalid_input_equivalence(self):
        """Both versions reject the same invalid input."""
        bad_data = {"name": "", "email": "not-email"}
        
        flask_response = flask_client.post("/users", json=bad_data)
        fastapi_response = fastapi_client.post("/users", json=bad_data)
        
        # Both should return 4xx
        assert flask_response.status_code == fastapi_response.status_code

Phase 4: Migrate — One Component at a Time

The anti-pattern: Big Bang

# DANGEROUS: migrate everything at once
Day 1: Rewrite the whole app in FastAPI
Day 2: "Why doesn't anything work?"
Day 3-30: Endless debugging

The correct pattern: Incremental

# SAFE: migrate endpoint by endpoint
Day 1: GET /health → FastAPI ✅ (test green)
Day 1: GET /users → FastAPI ✅ (test green)
Day 2: POST /users → FastAPI ✅ (test green)
Day 2: GET /users/:id → FastAPI ✅ (test green)
...
Day 5: POST /orders → FastAPI ✅ (test green)
Day 6: Remove Flask ✅ (all tests green)

Each step is reversible. Each step has verification. If on day 3 something fails, you know it's the endpoint you migrated on day 3 — you don't have to search across 12 endpoints.


Comparison: Migration Strategies

CriterionBig BangIncrementalStrangler Fig
SpeedFast at the start, slow at the endConstantConstant
RiskVery highLowVery low
RollbackAll or nothingPer endpointPer endpoint + routing
CoexistenceNoTemporaryDesigned
TestingAt the endAt each stepAt each step
When to useNever (in this context)Internal projectsPublic APIs, prod

Connection with the Project

In the Module Project (capsule 06) you're going to execute this whole cycle: assessment → plan → tests → migrate → validate. The plan you design here is the blueprint of the practical execution.


Troubleshooting

Problem 1: I don't know what order to migrate in

Solution: Increasing complexity: health check → simple GETs → simple POSTs → endpoints with auth → endpoints with complex logic.

Problem 2: The plan is too detailed for 6 endpoints

Solution: Proportional to the complexity. 6 endpoints = a 1-page plan. 60 endpoints = a 5-page plan with phases.

Problem 3: When do I do the cutover from Flask to FastAPI?

Solution: When 100% of the endpoints are migrated AND 100% of the equivalence tests pass. Not before.


Exercises

Exercise 1: Classify endpoints by complexity (Easy)

Classify these 6 endpoints from lowest to highest migration complexity:

  1. GET /api/status — returns {"status": "ok"}
  2. POST /api/auth/login — validates credentials, generates a JWT
  3. GET /api/users — lists users with pagination
  4. POST /api/orders — validates, calculates, charges, saves
  5. GET /api/products/:id — looks up by ID
  6. PUT /api/users/:id — updates with validation
See solution
  1. GET /api/status (Low — no logic)
  2. GET /api/products/:id (Low — read only)
  3. GET /api/users (Medium-Low — pagination)
  4. PUT /api/users/:id (Medium — validation + update)
  5. POST /api/auth/login (Medium-High — auth, JWT)
  6. POST /api/orders (High — validation + calculation + payment)

Migration order: 1→2→3→4→5→6

Exercise 2: Design a migration plan (Medium)

Write a migration plan for a Flask app with 8 endpoints, 2 Blueprints, and auth middleware. Use the format from the "Structure of the plan" section.

See solution
# Migration Plan: Flask → FastAPI

## Phase 1: Foundation (Day 1)
- Set up FastAPI + routers
- Migrate GET /health
- Checkpoint: FastAPI serves /health

## Phase 2: Read Endpoints (Day 2)
- Migrate 3 GET endpoints
- Migrate Blueprint 1 → APIRouter 1
- Checkpoint: 4/8 migrated, tests green

## Phase 3: Write + Auth (Day 3-4)
- Migrate auth middleware → Depends()
- Migrate 3 POST endpoints
- Migrate Blueprint 2 → APIRouter 2
- Checkpoint: 7/8 migrated, tests green

## Phase 4: Complex + Cleanup (Day 5)
- Migrate POST /orders (most complex)
- Remove Flask, update deps
- Checkpoint: 8/8, all tests green

## Rollback: git tag at each checkpoint

Exercise 3: Write an equivalence test (Medium)

Write an equivalence test for POST /api/users that verifies that Flask and FastAPI produce the same response for valid and invalid input.

See solution
class TestUserCreationEquivalence:
    def test_valid_user_same_response(self):
        data = {"name": "Ana", "email": "ana@test.com", "password": "secure123"}
        flask_r = flask_client.post("/api/users", json=data)
        fastapi_r = fastapi_client.post("/api/users", json=data)
        
        assert flask_r.status_code == fastapi_r.status_code
        assert flask_r.json["name"] == fastapi_r.json()["name"]
        assert flask_r.json["email"] == fastapi_r.json()["email"]
    
    def test_invalid_email_same_error(self):
        data = {"name": "Ana", "email": "bad", "password": "secure123"}
        flask_r = flask_client.post("/api/users", json=data)
        fastapi_r = fastapi_client.post("/api/users", json=data)
        
        assert flask_r.status_code == fastapi_r.status_code
        # Both should return 400/422
    
    def test_missing_field_same_error(self):
        data = {"name": "Ana"}  # missing email and password
        flask_r = flask_client.post("/api/users", json=data)
        fastapi_r = fastapi_client.post("/api/users", json=data)
        
        assert flask_r.status_code == fastapi_r.status_code

Summary

  • The migration cycle: PLAN → TEST → MIGRATE → VALIDATE (repeat)
  • Assessment first: inventory of endpoints, dependencies, complexity
  • Plan with phases and checkpoints: each phase has a success criterion and rollback
  • Safety net: equivalence tests BEFORE migrating
  • Always incremental: one endpoint at a time, tests at each step
  • Big bang never: too much risk, impossible debugging

Next capsule: Flask→FastAPI Step-by-Step. You're going to execute the practical migration with Claude Code.


Additional Resources

  1. Strangler Fig - Martin Fowler - The gradual migration pattern
  2. FastAPI vs Flask - Comparison - Official technical differences
  3. Migration Patterns - ThoughtWorks - Industry migration patterns
  4. Feature Toggles - Martin Fowler - Toggles for gradual migrations
  5. Blue-Green Deployments - A deployment strategy for migrations
  6. Canary Releases - Gradual releases that complement migrations

Module 5, Capsule 02 — Refactoring & Legacy Code with Claude Code Guide