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
| Component | Complexity | FastAPI equivalent | Suggested order |
|---|---|---|---|
| GET /health | Low | Direct | 1 |
| GET /users | Low | Direct | 2 |
| POST /users | Medium | Pydantic validation | 3 |
| Auth middleware | Medium | Depends() | 4 |
| POST /orders | High | Pydantic + Depends | 5 |
| Blueprints | Medium | APIRouter | 6 |
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
| Criterion | Big Bang | Incremental | Strangler Fig |
|---|---|---|---|
| Speed | Fast at the start, slow at the end | Constant | Constant |
| Risk | Very high | Low | Very low |
| Rollback | All or nothing | Per endpoint | Per endpoint + routing |
| Coexistence | No | Temporary | Designed |
| Testing | At the end | At each step | At each step |
| When to use | Never (in this context) | Internal projects | Public 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:
- GET /api/status — returns {"status": "ok"}
- POST /api/auth/login — validates credentials, generates a JWT
- GET /api/users — lists users with pagination
- POST /api/orders — validates, calculates, charges, saves
- GET /api/products/:id — looks up by ID
- PUT /api/users/:id — updates with validation
See solution
- GET /api/status (Low — no logic)
- GET /api/products/:id (Low — read only)
- GET /api/users (Medium-Low — pagination)
- PUT /api/users/:id (Medium — validation + update)
- POST /api/auth/login (Medium-High — auth, JWT)
- 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
- Strangler Fig - Martin Fowler - The gradual migration pattern
- FastAPI vs Flask - Comparison - Official technical differences
- Migration Patterns - ThoughtWorks - Industry migration patterns
- Feature Toggles - Martin Fowler - Toggles for gradual migrations
- Blue-Green Deployments - A deployment strategy for migrations
- Canary Releases - Gradual releases that complement migrations
Module 5, Capsule 02 — Refactoring & Legacy Code with Claude Code Guide