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 hint user_id: int
  • return 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=201 en el decorador
  • response_model documenta y valida la respuesta
  • EmailStr valida 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

FlaskFastAPINotas
@app.route("/path", methods=["GET"])@app.get("/path")Decorador por método
request.get_json()Parámetro con Pydantic modelAutomático
request.args.get("q")q: str = Query(None)Query parameters
jsonify({})return {}Serialización automática
return ..., 404raise HTTPException(404)Excepciones HTTP
@app.before_requestMiddleware o Depends()Dependency injection
Blueprint("name")APIRouter(prefix="/name")Routers
g.userDepends(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) reemplazan methods=[]
  • 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

  1. FastAPI - First Steps - Tutorial oficial paso a paso
  2. Pydantic v2 Documentation - Referencia completa de Pydantic
  3. FastAPI - Request Body - Cómo FastAPI maneja request bodies
  4. FastAPI - Path Parameters - Parámetros de ruta tipados
  5. Flask to FastAPI Migration Guide - Comparación oficial
  6. HTTPException - FastAPI - Manejo de errores en FastAPI