Índice de contenido
- Herramientas: Gemini Anti-Gravity (Alley)
- Preparación y Conceptos de Arquitectura
- ¿Qué es la Clean Architectures y Clean Code?
- Clean Code vs Clean Architecture
- El Prompt y la Refactorización
- Ejecución del Plan
- El Problema de los WebSockets
- Resultado
- Interface Adapters
- Use Cases (Casos de Uso)
- Entities (Entidades)
- Frameworks & Drivers
- En esta carpeta, puedes ver la implementación más fuerte de las tecnologías que NO son de nosotros y que podemos cambiar en cualquier momento, como viene siendo, FastAPI y la base de datos con SQLAlchemy.
Vamos a realizar un pequeño experimento. Tomaremos nuestro proyecto actual, el cual no sigue precisamente las mejores prácticas organizativas -ya que nació como una prueba rápida para implementar WebSockets-, y vamos a mejorar su estructura, y esto lo hacemos siguiendo las buenas prácticas como presentamos antes con El ciclo de vida de una app en FastAPI con los eventos Lifespan
Para ello, utilizaremos arquitecturas de software establecidas. No pretendo que esto sea un curso profundo de Go o de arquitecturas avanzadas, sino más bien una presentación para despertar tu curiosidad. Más adelante podremos dedicarle una sección completa o un curso específico, o si lo prefieres, puedes investigar por tu cuenta. Es el momento ideal para este experimento, ya que la aplicación es pequeña y manejable.
Herramientas: Gemini Anti-Gravity (Alley)
Para este trabajo utilizaré Gemini Anti-Gravity, el editor que ves en pantalla. Si no lo conoces, es similar a herramientas como Windsurf o muchas otras que están surgiendo. Es una bifurcación de VS Code que permite interactuar con la IA de manera más efectiva mediante agentes integrados en el espacio de trabajo.
Una de sus opciones más interesantes es la de Planning (Planificación). A diferencia de otras extensiones para VS Code, esta herramienta genera una "hoja de ruta" antes de aplicar cualquier cambio. Esto es extremadamente útil porque te permite revisar y ajustar el plan antes de que la IA modifique tu código.
Preparación y Conceptos de Arquitectura
Antes de pedir cambios globales a la IA, es fundamental sincronizar tu proyecto con GitHub. Las cosas pueden salir mal, y tener un respaldo te ahorrará mucho tiempo si necesitas revertir cambios.
¿Qué es la Clean Architectures y Clean Code?
Seguramente has oído hablar de la Clean Architectures, Arquitectura limpia y el Clean Code, vamos a ver que es cada uno de ellos.
Clean Code vs Clean Architecture
Es importante aclarar algo:
- Clean Code no es una arquitectura, es una filosofía.
- Se enfoca en escribir código simple, modular, legible y mantenible.
- Clean Architecture es la aplicación práctica de esos principios.
Es como la diferencia entre API y REST API: uno es el concepto general y el otro es su aplicación concreta.
En esencia el Clean Code, son formas de organizar el código basadas en buenos principios. Nosotros ya hemos aplicado algo de esto al separar esquemas y modelos, pero de forma incompleta. La idea de usar una arquitectura existente es no reinventar la rueda. En lugar de inventar nombres para nuestras carpetas, usamos estructuras probadas que hacen que el código sea legible, mantenible y escalable.
La estructura que buscamos generar se divide generalmente en:
- Entities (Entidades): La lógica de negocio más pura.
- Use Cases (Casos de Uso): Acciones específicas (ej. crear un usuario, login). Es el corazón de la lógica de negocio. Aquí se define qué pasa cuando un usuario hace login, independientemente de si la petición viene de una API REST o de un WebSocket.
- Aquí está la lógica real del sistema.
- Por ejemplo, en el login:
- Verificamos usuario.
- Validamos contraseña.
- Generamos token.
- Antes, esto estaba dentro del endpoint.
- Ahora está desacoplado.
- Eso permite que:
- No dependa del tipo de respuesta.
- No dependa de FastAPI.
- Pueda reutilizarse.
- Interface Adapters: Controladores y repositorios que traducen datos. Actúan como traductores. Aquí están los controladores que reciben peticiones HTTP y los repositorios que manejan los datos.
- Son los traductores.
- Reciben:
- Peticiones HTTP
- WebSocket
- XML
- JSON
- Y traducen esas entradas hacia los casos de uso.
- Frameworks & Drivers: Herramientas externas como la base de datos o el framework web (FastAPI). Aquí reside el código que "no es nuestro", como la configuración de FastAPI o la conexión a la base de datos (SQLAlchemy). Si mañana queremos cambiar FastAPI por Flask, solo deberíamos tocar esta capa.
- Aquí va el código que no es nuestro:
- FastAPI
- Conexión a base de datos
- ORM
- Mongo, SQLite, Firebase, etc.
- Si mañana cambiamos FastAPI por Flask, los cambios estarían aquí.
- Aquí va el código que no es nuestro:

El Prompt y la Refactorización
Para que la IA sea efectiva, necesitamos un buen Prompt. He utilizado una IA para generar la instrucción que le daremos a nuestro agente de código. El resultado fue el siguiente:
"Actúa como un experto en arquitectura de software. Refactoriza mi aplicación FastAPI siguiendo los principios de Clean Code. Separa el código en capas de Entities, Use Cases, Interfaces y Adapters. Implementa el patrón Repositorio para el acceso a datos, asegurando que las dependencias apunten hacia adentro. Genera la nueva estructura de carpetas y los archivos correspondientes."
Ejecución del Plan
Al activar la opción de Planning, la IA nos muestra qué archivos creará y qué carpetas moverá. En este caso, creará una carpeta src con subcarpetas para entidades, casos de uso (como login_use_case.py) y repositorios.
El Problema de los WebSockets
Durante la refactorización, hubo un pequeño error con los WebSockets debido a la confusión entre versiones del proyecto. La IA inicialmente generó un socket muy simple que solo devolvía texto. Tuve que pedirle una corrección específica:
"Adapta el endpoint de WebSockets llamado websocket_endpoint a la arquitectura limpia, integrando el manejador de conexiones (ConnectionManager) que teníamos anteriormente."
Resultado
Veamos algunos códigos importantes para entender la estructura anterior:
Archivo en su mínima expresión que es el punto de entrada de la aplicación, en el cual, accemos a los controladores:
main.py
"""Application entry point."""
from src.frameworks_drivers.http.app import appsrc/frameworks_drivers/http/app.py
from src.interface_adapters.controllers import (
auth_controller,
alerts_controller,
rooms_controller,
websocket_controller
)
***
# Include routers with /api prefix
app.include_router(auth_controller.router, prefix="/api")
app.include_router(alerts_controller.router, prefix="/api")
app.include_router(rooms_controller.router, prefix="/api")Interface Adapters
Aquí se encuentran los controladores, que es la puerta de entrada de nuestra app, aunque, si es cierto que resulta imposible cumplir al 100% con los principios del Clean Code, en esta capa NO debería haber nada de código de FastAPI, aquí tenemos los controladores de FastAPI, que en resumen, siguiendo el esquema MVC, es la capa imptermedia, la de control que conecta la Vista con los Modelos:
src/interface_adapters/controllers/alerts_controller.py
@router.get("/alerts", response_model=List[Alert])
def get_alerts(
room_id: Optional[int] = None,
user: User = Depends(get_current_user),
alert_repo=Depends(get_alert_repository)
):
"""Get alerts endpoint with optional room filtering."""
use_case = GetAlertsUseCase(alert_repo)
alerts = use_case.execute(room_id=room_id)
# Convert entities to ORM-compatible format for Pydantic
return [
{
"id": alert.id,
"content": alert.content,
"created_at": alert.created_at,
"user_id": alert.user_id
}
for alert in alerts
]En el ejemplo de controlador anterior, vemos que la base de datos se inyecta como una dependencia, ya que, puede ser cualquier cosa, una base de datos en MariDB, PSQL, un JSON, Firebase, un archivo y por los principios del Clean Code, se maneja de esta forma para que sea débilmente acoplada, lo que significa, que podemos cambiar la fuente de datos sin romper con los controladores u otras capas.
Ademas, usamos los casos de usos en donde se maneja la lógica de negocios.
Use Cases (Casos de Uso)
src/use_cases/auth/login.py
"""Login Use Case - Handles user authentication."""
from typing import Optional
import bcrypt
from src.entities.user import User
from src.entities.token import Token
from src.interface_adapters.repositories.repository_interfaces import (
UserRepositoryInterface,
TokenRepositoryInterface
)
class LoginUseCase:
"""Use case for user login."""
def __init__(
self,
user_repository: UserRepositoryInterface,
token_repository: TokenRepositoryInterface
):
self.user_repository = user_repository
self.token_repository = token_repository
def execute(self, username: str, password: str) -> Optional[str]:
"""
Execute login use case.
Args:
username: User's username
password: User's plain password
Returns:
Token key if successful, None otherwise
"""
# Get user by username
user = self.user_repository.get_by_username(username)
if not user:
return None
# Verify password
if not self._verify_password(password, user.password):
return None
# Get or create token
token = self.token_repository.get_by_user_id(user.id)
if not token:
import secrets
token = Token(
key=secrets.token_hex(20),
user_id=user.id
)
token = self.token_repository.create(token)
return token.key
@staticmethod
def _verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify password against hash."""
password_byte_enc = plain_password.encode('utf-8')
hashed_password_enc = hashed_password.encode('utf-8')
return bcrypt.checkpw(password_byte_enc, hashed_password_enc)En este módulo tenemos la lógica de nuestra empresa o negocio, no le importa de donde salen los datos ni el formato esperado, esta capa usa una entidad genética (ni los modelos de Pydantic ni el ORM); al ser la lógica de negocios, en nuestro caso, un usuario autenticado, manejamos los casos que cubriamos en la Rest API:
rest_api.py
@router.post("/login")
def login(request: schemas.LoginRequest, db: Session = Depends(get_db)):
user = db.query(models.User).filter(models.User.username == request.username).first()
if not user:
return JSONResponse("User invalid", status_code=status.HTTP_401_UNAUTHORIZED)
if not verify_password(request.password, user.password):
return JSONResponse("Password invalid", status_code=status.HTTP_401_UNAUTHORIZED)
# Get or Create Token
token = db.query(models.Token).filter(models.Token.user_id == user.id).first()
if not token:
token = models.Token(user_id=user.id)
db.add(token)
db.commit()
db.refresh(token)
return {"token": f"Token_{token.key}"}Pero con una mayor abstracción, como comentamos antes, a los casos de uso NO le importa el fuente de datos (Base de datos u otro) o el tipo de retorno (que no es especifico, ya que, pudieramos emplear este caso de uso como respuesta de una API -lo que tenemos- o desde un template con Jinja u otro…) y usa una entidad genérica nuevamente NO acoplada a ningun framework que es la siguiente capa.
Entities (Entidades)
Tenemos 3 tipos de entidades, las dos que teníamos en nuestro proyecto de FastAPI que están fuertemente acopladas a la tecnología, que en este ejemplo es FastAPI:
src/frameworks_drivers/db/orm_models.py
src/interface_adapters/presenters/schemas.py
Que están definidas en el ORM (fuertemente ligado a código que no es de nosotros como lo es la base de datos con SQLAlchemy) y los esquemas de Pydantic (que aunque son usados por FastAPI, por definición, son traductores para el proyecto de FastAPI)
Y finalmente, tenemos clases aisladas, de tipo data class, que al igual que en Kotlin son clases cuyo propósito es presentar los datos:
src/entities
"""Alert entity - Core business model."""
from dataclasses import dataclass
from datetime import datetime
from typing import Optional
@dataclass
class Alert:
"""Alert entity representing a message in a room."""
id: Optional[int]
content: str
user_id: int
room_id: int
created_at: Optional[datetime] = NoneY por lo tanto, siguiendo con los principios del Clean Code, las aplicaciones deben de tener un acoplamiento debil y por ende, podemos cambiar de framework o tecnología (pasar a Django o Flask que NO utilizan modelos de Pydantic) que los casos de usos NO se verán alterados, lo cual si sucedería si empleamos en lugar de estas entidades una clase de Pydantic.
Frameworks & Drivers
En esta carpeta, puedes ver la implementación más fuerte de las tecnologías que NO son de nosotros y que podemos cambiar en cualquier momento, como viene siendo, FastAPI y la base de datos con SQLAlchemy.
Código fuente:
https://github.com/libredesarrollo/curso-libro-django-vue-channels