Module 5: Framework and Language Migration
Module Project: Flask→FastAPI Migration
Module Project: Flask→FastAPI Migration
Project description
This is the project that consolidates everything you learned in the module: planning a migration, executing it endpoint by endpoint, writing equivalence tests, and producing a FastAPI app that does exactly the same as the original Flask one. It's not a theoretical exercise — it's a complete migration with functional code.
You're going to receive a Flask app with 5 endpoints, manual validation, and error handling. Your job is to migrate it to FastAPI step-by-step: one endpoint at a time, with equivalence tests at each step. In the end, both apps must produce identical responses for the same inputs.
This project connects with Module 8 (Capstone Project) where the framework migration can be part of the full modernization of a legacy project.
Project Objective
Migrate a complete Flask app to FastAPI, verifying equivalence at each step.
By completing it:
- ✅ You'll have designed a migration plan with phases
- ✅ You'll have migrated 5 endpoints Flask→FastAPI
- ✅ You'll have written equivalence tests for each endpoint
- ✅ You'll have used Pydantic to replace manual validation
- ✅ You'll have documented the complete process
Technical Specifications
Flask App to Migrate
Use the following Flask app (or create a similar one):
# flask_app.py
from flask import Flask, request, jsonify
from datetime import datetime
app = Flask(__name__)
products = {}
orders = {}
pid_counter = 1
oid_counter = 1
@app.route("/health")
def health():
return jsonify({"status": "ok", "timestamp": datetime.now().isoformat()})
@app.route("/products", methods=["GET"])
def list_products():
return jsonify(list(products.values()))
@app.route("/products", methods=["POST"])
def create_product():
data = request.get_json()
if not data.get("name") or len(data["name"]) < 2:
return jsonify({"error": "Name must be 2+ characters"}), 400
if not isinstance(data.get("price"), (int, float)) or data["price"] <= 0:
return jsonify({"error": "Price must be positive number"}), 400
global pid_counter
product = {"id": pid_counter, "name": data["name"],
"price": round(float(data["price"]), 2),
"created_at": datetime.now().isoformat()}
products[pid_counter] = product
pid_counter += 1
return jsonify(product), 201
@app.route("/products/<int:product_id>")
def get_product(product_id):
product = products.get(product_id)
if not product:
return jsonify({"error": "Product not found"}), 404
return jsonify(product)
@app.route("/orders", methods=["POST"])
def create_order():
data = request.get_json()
if not data.get("items") or not isinstance(data["items"], list):
return jsonify({"error": "Items must be a non-empty list"}), 400
total = 0
order_items = []
for item in data["items"]:
pid = item.get("product_id")
qty = item.get("quantity", 1)
if pid not in products:
return jsonify({"error": f"Product {pid} not found"}), 404
if qty < 1:
return jsonify({"error": "Quantity must be >= 1"}), 400
product = products[pid]
line_total = product["price"] * qty
total += line_total
order_items.append({"product_id": pid, "name": product["name"],
"quantity": qty, "line_total": round(line_total, 2)})
global oid_counter
order = {"id": oid_counter, "items": order_items,
"total": round(total, 2), "status": "confirmed",
"created_at": datetime.now().isoformat()}
orders[oid_counter] = order
oid_counter += 1
return jsonify(order), 201
Setup
mkdir flask-to-fastapi && cd flask-to-fastapi
python -m venv venv && source venv/bin/activate
pip install flask fastapi uvicorn pydantic pytest httpx
Deliverables
1. Migration Plan (MIGRATION_PLAN.md)
# Migration Plan
## Assessment
- [Endpoint inventory with complexity]
## Phases
- [Phase 1: Foundation]
- [Phase 2: Simple endpoints]
- [Phase 3: Complex endpoints]
- [Phase 4: Cleanup]
## Rollback Strategy
- [Plan B for each phase]
2. Migrated FastAPI App (fastapi_app.py)
The complete FastAPI app with all the endpoints migrated, Pydantic models, and error handling.
3. Equivalence Tests (tests/test_equivalence.py)
Parametrized tests that verify that Flask and FastAPI produce the same responses.
4. Migration Log (MIGRATION_LOG.md)
# Migration Log
## Endpoint 1: GET /health
- **Flask:** 3 lines, no logic
- **FastAPI:** 3 lines, direct
- **Differences:** None
- **Tests:** ✅ Equivalence verified
## Endpoint 2: ...
[Repeat for each endpoint]
## Metrics
| Metric | Value |
|---------|-------|
| Endpoints migrated | 5/5 |
| Equivalence tests | X |
| Total time | X min |
| Flask lines | X |
| FastAPI lines | X |
Success Criteria
- ✅ 5/5 endpoints migrated to FastAPI
- ✅ Pydantic models replace manual validation
- ✅ Equivalence tests pass for all the endpoints
- ✅ Tests cover happy paths AND error cases
- ✅ Migration plan documented
- ✅ Migration log with detail per endpoint
Evaluation Rubric (100 points)
Migration (40 points)
- (8 pts) GET /health migrated correctly
- (8 pts) GET /products migrated
- (8 pts) POST /products with a Pydantic model
- (8 pts) GET /products/{id} with HTTPException
- (8 pts) POST /orders with Pydantic + logic
Testing (30 points)
- (10 pts) Equivalence tests for happy paths
- (10 pts) Equivalence tests for error cases
- (10 pts) Parametrized tests where applicable
Documentation (20 points)
- (10 pts) Migration plan with phases and rollback
- (10 pts) Migration log with details per endpoint
Process (10 points)
- (5 pts) Incremental migration (not big bang)
- (5 pts) Tests run after each endpoint
Extra Credit (+10 points)
- (+3 pts) Strangler fig proxy implemented
- (+3 pts) Additional contract tests
- (+2 pts) Custom Pydantic validators
- (+2 pts) Response models for all the endpoints
Common Errors
Error 1: Migrating everything at once without tests between steps
Migrate one endpoint, run tests, migrate the next. Not 5 at once.
Error 2: Changing the business logic during the migration
The total is computed the same. The validations are the same. Only the framework changes.
Error 3: Not handling the 400 vs 422 difference
FastAPI/Pydantic returns 422 for validation errors. Decide whether you accept it or change it to 400.
Error 4: Forgetting status_code=201 in POST
Flask: return jsonify(data), 201. FastAPI: @app.post("/path", status_code=201).
Error 5: Not comparing structure, only status code
A test that only verifies the status code doesn't detect whether the body changed.
What to Do if You Get Stuck?
If Pydantic rejects a payload that Flask accepted:
→ Check the types in the model (Optional, Union)
→ Flask was probably too permissive (that's GOOD)
→ Decide: relax Pydantic, or document the change in MIGRATION_LOG
If the equivalence tests fail on error cases:
→ It's expected: Flask=400, FastAPI=422 by default
→ Compare the "class" of the error (4xx), not the exact code
→ If you need exact idempotence, customize the exception handler
If the order calculation logic differs by 1 cent:
→ Rounding difference: round() vs Decimal
→ Keep the same Flask technique in FastAPI
→ Don't mix float and Decimal in the same flow
If Claude Code generates FastAPI code that doesn't compile:
→ Probably a version mismatch (Pydantic v1 vs v2)
→ Specify Pydantic v2 explicitly in the prompt
→ Pin the version in requirements.txt: pydantic>=2.0,<3.0
Evidence of Success (Self-Verification)
Before declaring the project complete, validate that you meet these checkpoints:
Functionality
- ✅ The 5 endpoints respond with the same structure as Flask for valid inputs
- ✅ POST /products with
{"name": "ab", "price": 10.5}returns 201 with the product - ✅ POST /orders with invalid items returns 4xx (not 500)
- ✅ GET /products/9999 returns 404 with a readable message
Equivalence
- ✅ Parametrized tests cover at least 3 endpoints in GET and 2 in POST
- ✅ For each endpoint, there's at least 1 error-case test (not just a happy path)
- ✅ The 400 vs 422 difference is documented in MIGRATION_LOG (it's not a bug)
Documentation
- ✅ MIGRATION_PLAN.md has phases with a "done" criterion per phase
- ✅ MIGRATION_LOG.md documents each endpoint with differences and decisions
- ✅ The final metrics are in MIGRATION_LOG (lines, time, tests)
Process
- ✅ There are at least 5 commits (one per migrated endpoint, minimum)
- ✅ Each commit had green tests at the time
If the 12 points are in place, the project is at the professional bootcamp level. If you doubt any of them, review the corresponding capsule before marking the project as complete.
Resources for the Project
- FastAPI Tutorial - The official step-by-step tutorial
- Pydantic v2 - Pydantic documentation
- pytest - The testing framework
- httpx - HTTP client for async tests
- FastAPI TestClient - Testing FastAPI
- FastAPI vs Flask Comparison - Official differences table
Connection with the Next Module
Module 6: Context Management solves the problem that emerges when the project is large: how do you handle 50K+ lines with Claude Code's context window? The migration techniques you learned here work for small projects. Module 6 gives you the tools to scale to real projects — and Module 8 (Capstone Project) applies them all together in a complete legacy project migration.