Módulo 5: Migración de Frameworks y Lenguajes
Flask a FastAPI — Migración Step-by-Step
Flask a FastAPI — Migración Step-by-Step
Descripción de la cápsula
Esta es la cápsula más práctica del módulo. Vas a migrar una aplicación Flask a FastAPI endpoint por endpoint, con Claude Code haciendo la conversión y tú verificando cada paso. No es teoría sobre migración — es ejecución con código real que puedes replicar en tu terminal.
El caso Flask→FastAPI es ideal para aprender porque los frameworks son lo suficientemente similares para que la migración sea comprensible, pero lo suficientemente diferentes para que la conversión requiera cambios reales: decoradores, validación (manual→Pydantic), async, dependency injection, y testing.
Claude Code es especialmente valioso aquí porque conoce las APIs de ambos frameworks y puede hacer la conversión semántica: no solo reemplaza @app.route con @app.get, sino que convierte Flask patterns a sus equivalentes idiomáticos en FastAPI.
Setup: Dos Apps Lado a Lado
La app Flask original
# flask_app.py
from flask import Flask, request, jsonify
app = Flask(__name__)
users_db = {}
next_id = 1
@app.route("/health", methods=["GET"])
def health():
return jsonify({"status": "healthy", "framework": "flask"})
@app.route("/users", methods=["GET"])
def get_users():
return jsonify(list(users_db.values()))
@app.route("/users/<int:user_id>", methods=["GET"])
def get_user(user_id):
user = users_db.get(user_id)
if not user:
return jsonify({"error": "User not found"}), 404
return jsonify(user)
@app.route("/users", methods=["POST"])
def create_user():
data = request.get_json()
if not data.get("name"):
return jsonify({"error": "Name required"}), 400
if not data.get("email") or "@" not in data["email"]:
return jsonify({"error": "Valid email required"}), 400
global next_id
user = {
"id": next_id,
"name": data["name"],
"email": data["email"]
}
users_db[next_id] = user
next_id += 1
return jsonify(user), 201
@app.route("/users/<int:user_id>", methods=["PUT"])
def update_user(user_id):
user = users_db.get(user_id)
if not user:
return jsonify({"error": "User not found"}), 404
data = request.get_json()
if "name" in data:
user["name"] = data["name"]
if "email" in data:
if "@" not in data["email"]:
return jsonify({"error": "Valid email required"}), 400
user["email"] = data["email"]
return jsonify(user)
Crear el proyecto FastAPI vacío
pip install fastapi uvicorn pydantic
# fastapi_app.py (vacío, lo llenaremos endpoint por endpoint)
from fastapi import FastAPI
app = FastAPI()
# Los endpoints se migran uno a la vez aquí
Migración Endpoint por Endpoint
Endpoint 1: GET /health (Complejidad: Mínima)
# Prompt a Claude Code:
> "Migra el endpoint GET /health de flask_app.py a
fastapi_app.py. Convierte a FastAPI idiomático."
Flask:
@app.route("/health", methods=["GET"])
def health():
return jsonify({"status": "healthy", "framework": "flask"})
FastAPI:
@app.get("/health")
def health():
return {"status": "healthy", "framework": "fastapi"}
Diferencias clave:
@app.route("/health", methods=["GET"])→@app.get("/health")jsonify({})→{}(FastAPI serializa automáticamente)- Cambio de "flask" a "fastapi" en el response (intencional)
Verificación:
> "Ejecuta el test de equivalencia para GET /health"
Endpoint 2: GET /users (Complejidad: Baja)
> "Migra GET /users de Flask a FastAPI"
Flask:
@app.route("/users", methods=["GET"])
def get_users():
return jsonify(list(users_db.values()))
FastAPI:
@app.get("/users")
def get_users():
return list(users_db.values())
Diferencia: Solo el decorador y eliminar jsonify.
Endpoint 3: GET /users/{id} (Complejidad: Baja-Media)
> "Migra GET /users/<user_id> de Flask a FastAPI.
Convierte el manejo de error 404 a HTTPException."
Flask:
@app.route("/users/<int:user_id>", methods=["GET"])
def get_user(user_id):
user = users_db.get(user_id)
if not user:
return jsonify({"error": "User not found"}), 404
return jsonify(user)
FastAPI:
from fastapi import HTTPException
@app.get("/users/{user_id}")
def get_user(user_id: int):
user = users_db.get(user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
Diferencias:
<int:user_id>→{user_id}con type hintuser_id: intreturn jsonify(...), 404→raise HTTPException(404, ...)- FastAPI valida el tipo automáticamente (si pasas string, retorna 422)
Endpoint 4: POST /users (Complejidad: Media)
Este es el primer endpoint con validación real — aquí Pydantic brilla.
> "Migra POST /users de Flask a FastAPI. Convierte la
validación manual a un Pydantic model. Usa response_model
para documentar el tipo de respuesta."
Flask (validación manual):
@app.route("/users", methods=["POST"])
def create_user():
data = request.get_json()
if not data.get("name"):
return jsonify({"error": "Name required"}), 400
if not data.get("email") or "@" not in data["email"]:
return jsonify({"error": "Valid email required"}), 400
# ... crear usuario
FastAPI (validación con Pydantic):
from pydantic import BaseModel, EmailStr
class UserCreate(BaseModel):
name: str # Pydantic valida automáticamente que no esté vacío
email: EmailStr # Pydantic valida formato de email
class UserResponse(BaseModel):
id: int
name: str
email: str
@app.post("/users", response_model=UserResponse, status_code=201)
def create_user(user_data: UserCreate):
global next_id
user = {
"id": next_id,
"name": user_data.name,
"email": user_data.email
}
users_db[next_id] = user
next_id += 1
return user
Diferencias significativas:
request.get_json()manual → Pydantic model automático- Validación inline (if/else) → type hints + Pydantic
status_code=201en el decoradorresponse_modeldocumenta y valida la respuestaEmailStrvalida email mejor que"@" in email
Endpoint 5: PUT /users/{id} (Complejidad: Media)
> "Migra PUT /users/<user_id> de Flask a FastAPI.
Usa Pydantic model con campos opcionales para el update."
FastAPI:
class UserUpdate(BaseModel):
name: str | None = None
email: EmailStr | None = None
@app.put("/users/{user_id}", response_model=UserResponse)
def update_user(user_id: int, user_data: UserUpdate):
user = users_db.get(user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
if user_data.name is not None:
user["name"] = user_data.name
if user_data.email is not None:
user["email"] = user_data.email
return user
Mapping Flask → FastAPI
| Flask | FastAPI | Notas |
|---|---|---|
@app.route("/path", methods=["GET"]) | @app.get("/path") | Decorador por método |
request.get_json() | Parámetro con Pydantic model | Automático |
request.args.get("q") | q: str = Query(None) | Query parameters |
jsonify({}) | return {} | Serialización automática |
return ..., 404 | raise HTTPException(404) | Excepciones HTTP |
@app.before_request | Middleware o Depends() | Dependency injection |
Blueprint("name") | APIRouter(prefix="/name") | Routers |
g.user | Depends(get_current_user) | DI en vez de global |
abort(404) | raise HTTPException(404) | Excepciones |
Generando la Migración Completa con Claude Code
El prompt maestro
> "Migra la app Flask completa en flask_app.py a FastAPI
en fastapi_app.py. Para cada endpoint:
1. Convierte el decorador
2. Convierte validación manual a Pydantic models
3. Convierte error handling a HTTPException
4. Convierte request.get_json() a parámetros tipados
5. Elimina jsonify() (FastAPI serializa automáticamente)
Crea los Pydantic models necesarios al inicio del archivo.
Mantén la misma lógica de negocio."
Conexión con Proyecto
En el Proyecto del Módulo (cápsula 06) vas a ejecutar esta migración completa en una app Flask proporcionada. Esta cápsula te dio las técnicas endpoint por endpoint. El proyecto te pide hacerlo de punta a punta.
Troubleshooting
Problema 1: FastAPI retorna 422 en vez de 400
Causa: Pydantic validation errors retornan 422 (Unprocessable Entity), no 400.
Solución: 422 es técnicamente más correcto. Si necesitas 400, agrega un exception handler custom.
Problema 2: Los IDs son diferentes entre Flask y FastAPI
Causa: Si usas auto-increment, cada app genera IDs independientes.
Solución: En tests de equivalencia, compara estructura y campos, no IDs.
Problema 3: Flask acepta inputs que FastAPI rechaza
Causa: Pydantic es más estricto que validación manual.
Solución: Esto es una mejora. Documéntala. Si necesitas backward compatibility, relaja el Pydantic model.
Ejercicios
Ejercicio 1: Convertir decorador Flask (Fácil)
Convierte estos decoradores Flask a FastAPI:
@app.route("/products", methods=["GET"])
@app.route("/products/<int:pid>", methods=["DELETE"])
@app.route("/orders", methods=["POST"])
Ver solución
@app.get("/products")
@app.delete("/products/{pid}")
@app.post("/orders")
Ejercicio 2: Crear Pydantic model (Medio)
Convierte esta validación manual Flask a un Pydantic model:
data = request.get_json()
if not data.get("title") or len(data["title"]) < 3:
return jsonify({"error": "Title must be 3+ chars"}), 400
if not isinstance(data.get("price"), (int, float)) or data["price"] <= 0:
return jsonify({"error": "Price must be positive"}), 400
if data.get("category") not in ["electronics", "books", "clothing"]:
return jsonify({"error": "Invalid category"}), 400
Ver solución
from pydantic import BaseModel, Field
from typing import Literal
class ProductCreate(BaseModel):
title: str = Field(min_length=3)
price: float = Field(gt=0)
category: Literal["electronics", "books", "clothing"]
Pydantic valida todo automáticamente. 3 líneas reemplazan 6 líneas de validación manual.
Ejercicio 3: Migrar endpoint completo (Difícil)
Migra este endpoint Flask completo a FastAPI:
@app.route("/orders", methods=["POST"])
def create_order():
data = request.get_json()
if not data.get("user_id"):
return jsonify({"error": "user_id required"}), 400
if not data.get("items") or len(data["items"]) == 0:
return jsonify({"error": "items required"}), 400
total = sum(item["price"] * item.get("qty", 1) for item in data["items"])
if total <= 0:
return jsonify({"error": "Total must be positive"}), 400
order = {"id": next_order_id(), "user_id": data["user_id"],
"items": data["items"], "total": round(total, 2), "status": "pending"}
orders_db[order["id"]] = order
return jsonify(order), 201
Ver solución
from pydantic import BaseModel, Field
from typing import List
class OrderItem(BaseModel):
price: float = Field(gt=0)
qty: int = Field(default=1, ge=1)
class OrderCreate(BaseModel):
user_id: int
items: List[OrderItem] = Field(min_length=1)
class OrderResponse(BaseModel):
id: int
user_id: int
items: list
total: float
status: str
@app.post("/orders", response_model=OrderResponse, status_code=201)
def create_order(order_data: OrderCreate):
total = sum(item.price * item.qty for item in order_data.items)
if total <= 0:
raise HTTPException(status_code=400, detail="Total must be positive")
order = {
"id": next_order_id(),
"user_id": order_data.user_id,
"items": [item.model_dump() for item in order_data.items],
"total": round(total, 2),
"status": "pending"
}
orders_db[order["id"]] = order
return order
Resumen
- Flask→FastAPI se migra endpoint por endpoint, de simple a complejo
- Pydantic reemplaza validación manual con type hints declarativos
- HTTPException reemplaza
return ..., status_code - Decoradores por método (
@app.get,@app.post) reemplazanmethods=[] - Claude Code convierte semánticamente, no solo sintácticamente
- Verificar equivalencia después de cada endpoint
Próxima cápsula: Strangler Fig Pattern — cómo hacer que Flask y FastAPI coexistan durante la migración.
Recursos Adicionales
- FastAPI - First Steps - Tutorial oficial paso a paso
- Pydantic v2 Documentation - Referencia completa de Pydantic
- FastAPI - Request Body - Cómo FastAPI maneja request bodies
- FastAPI - Path Parameters - Parámetros de ruta tipados
- Flask to FastAPI Migration Guide - Comparación oficial
- HTTPException - FastAPI - Manejo de errores en FastAPI