


Haz que la IA escriba servicios FastAPI y detecte errores comunes al revisar
Úsalo cuando una IA crea o revisa un servicio FastAPI. Fija las versiones mínimas, cuándo usar rutas async o sync, cómo escribir modelos Pydantic y dependencias, cómo trabajar con la base de datos y las pruebas, y da una tabla de errores comunes en un diff con su corrección.
Para usar estas reglas de IA
## Fija las versiones mínimas de Python, FastAPI y las bibliotecas
Fija cada dependencia a la versión indicada abajo o una posterior; toda la guía aquí presupone estas líneas base. Una versión más antigua rompe silenciosamente los patrones de las secciones siguientes: las API async, las dependencias con `Annotated` y la serialización v2.
| Dependencia | Mínimo | Por qué este mínimo |
|---|---|---|
| Python | 3.11 | `StrEnum` y el operador de unión `X \| Y` |
| FastAPI | 0.115 | `Annotated[T, Depends(...)]` es la forma idiomática |
| Pydantic | 2.7 | Se eliminan las API de v1 (`json_encoders`, `.dict()`) |
| pydantic-settings | 2.4 | Paquete independiente desde Pydantic v2 |
| SQLAlchemy | 2.0 | La API async (`AsyncSession`, `async_sessionmaker`) |
| Alembic | 1.13 | Migraciones compatibles con async |
| httpx | 0.27 | `ASGITransport` para pruebas en proceso |
| PyJWT | 2.9 | Mantenido activamente; reemplaza a `python-jose` |
| ruff | 0.6 | Una sola herramienta para lint, formato y orden de imports |
## Organiza el proyecto en carpetas por dominio, no por tipo de archivo
Organiza el árbol de carpetas por dominio (contexto delimitado), no por tipo de archivo. Da a cada dominio su propio paquete y mantén una capa global delgada para los aspectos compartidos.
- Paquete por dominio: `router.py` (endpoints), `schemas.py` (modelos Pydantic), `models.py` (modelos ORM), `service.py` (lógica de negocio), `dependencies.py`, `config.py` (configuración específica del dominio), `constants.py`, `exceptions.py`, `utils.py`.
- Nivel global superior: `config.py` (configuración global), `models.py` (clases base compartidas), `exceptions.py`, `database.py` (motor async más fábrica de sesiones), `main.py` (app más lifespan).
- Imports entre dominios: importa siempre el módulo explícito y ponle un alias (por ejemplo, importa el `service` de un dominio como `auth_service`). No uses nunca un import con comodín ni accedas a través de una ruta interna profunda; ambas prácticas acoplan los dominios y bloquean la refactorización.
## Elige rutas async def o def para que la app no se bloquee
Elige el estilo de ruta según el trabajo que realiza; la elección equivocada congela el bucle de eventos o desperdicia hilos.
| La ruta hace esto | Usa |
|---|---|
| E/S no bloqueante y esperable (awaitable) | `async def` |
| E/S bloqueante, sin cliente async disponible | `def` (sync, se ejecuta en el threadpool) |
| Una mezcla de ambas | `async def` más `run_in_threadpool` para la llamada bloqueante |
| Trabajo limitado por CPU de más de ~50 ms | Delegar a un proceso worker (Celery / Arq / RQ) |
Nunca pongas una llamada bloqueante —`time.sleep`, `requests`, un driver de base de datos sync o un simple `open()`— dentro de `async def`; congela todas las peticiones de ese worker. Una ruta sync `def` solo bloquea a un worker del threadpool, así que es el lugar seguro para las llamadas bloqueantes inevitables. Dentro de `async def`, usa equivalentes esperables (`asyncio.sleep`, `aiofiles`, un driver async) o envuelve una librería sync con `run_in_threadpool`. Advertencias: el threadpool por defecto tiene unos 40 hilos, así que saturarlo ralentiza todas las rutas sync, y los hilos cuestan más que las corrutinas — no elijas rutas sync sin una razón bloqueante que lo justifique.
## Valida datos con Pydantic y separa la configuración por dominio
- Prefiere los validadores y tipos incorporados frente a las comprobaciones hechas a mano: `Field(min_length=, max_length=, pattern=, ge=)`, `EmailStr`, `AnyUrl` y `StrEnum` para conjuntos cerrados.
- No combines una restricción con un valor por defecto que la contradiga. `Field(ge=18, default=None)` es un error: decide si es obligatorio (`Field(ge=18)`) u opcional (`int | None = Field(default=None, ge=18)`).
- Usa serialización moderna. `json_encoders` está obsoleto en Pydantic v2: reemplázalo con `@field_serializer` para reglas por campo, o anota el tipo con `PlainSerializer`.
- Divide la configuración por dominio. `pydantic-settings` es un paquete independiente; da a cada dominio una subclase de `BaseSettings` con su propio `env_prefix` en lugar de un único objeto de configuración global que lean todos los dominios.
## Escribe dependencias con Annotated y haz que validen los datos
- Inyecta con `Annotated[T, Depends(fn)]`, la forma idiomática desde FastAPI 0.95. Evita la forma de argumento por defecto (`param: T = Depends(fn)`); es legado y arrastra trampas relacionadas con los valores por defecto.
- Haz que las dependencias validen, no solo inyecten. Una dependencia puede cargar un recurso por id, lanzar un 404 de dominio cuando no existe y devolver el objeto validado, de modo que la ruta reciba algo ya comprobado.
- Encadena dependencias para reutilizar lógica. Una dependencia puede depender de otras — por ejemplo, una comprobación de propiedad que depende de una dependencia de carga por id más una dependencia de token ya analizado.
- Reglas: las dependencias se cachean por petición, así que el mismo `Depends(x)` usado varias veces en una petición se ejecuta una sola vez. Prefiere dependencias `async def`; las sync se ejecutan en el threadpool, lo que supone una sobrecarga desperdiciada para comprobaciones pequeñas solo de CPU. Reutiliza una dependencia entre endpoints dando a la variable de ruta compartida el mismo nombre.
## Gestiona la autenticación JWT y el acceso asíncrono a la base de datos
- JWT: usa `PyJWT` (`import jwt`), no el ya no mantenido `python-jose`. Decodifica dentro de una protección sobre `InvalidTokenError` y lanza un error de credenciales de dominio en lugar de dejar escapar la excepción sin procesar.
- Sesiones: prefiere la API async de SQLAlchemy 2.0 (`create_async_engine`, `async_sessionmaker`, `AsyncSession`) y entrega las sesiones a las rutas mediante una dependencia generadora async `get_db`. `encode/databases` está en modo de mantenimiento; no lo elijas para proyectos nuevos.
- Nomenclatura: usa `lower_case_snake`; nombres de tabla en singular (`post`, `user`, `post_like`); agrupa las tablas relacionadas con un prefijo compartido (`payment_account`, `payment_bill`); usa el sufijo `_at` para datetimes y `_date` para fechas; y reutiliza el mismo nombre de columna de clave foránea en todos los lugares donde aparezca.
- Establece una única convención de nombres de índices y restricciones para todo el proyecto en el `MetaData`, de modo que los nombres generados sean predecibles.
- Primero SQL, después Pydantic: haz los joins, la agregación y el formateo de JSON en SQL, e hidrata a Pydantic solo para la validación de la respuesta, no para la transformación.
## Elige entre tareas en segundo plano y una cola como Celery
Elige la herramienta según el costo de la tarea y sus necesidades de fiabilidad.
| Usa tareas en segundo plano en el mismo proceso cuando | Usa una cola real (Celery / Arq / RQ) cuando |
|---|---|
| La tarea termina en menos de un segundo | Se ejecuta durante segundos o minutos |
| Es aceptable perderla silenciosamente | Necesitas reintentos, cola de mensajes fallidos o visibilidad |
| Es en el mismo proceso (enviar un correo, registrar una fila) | Es intensiva en CPU o necesita su propio pool |
| No se requiere programación | Necesitas cron, ETA o limitación de tasa |
Las tareas en segundo plano dentro del mismo proceso se ejecutan después de enviar la respuesta, en el mismo proceso worker. Si ese worker muere, la tarea se pierde y no hay reintento, así que nunca las uses para nada por lo que avisarías a un ingeniero de guardia.
## Prueba con httpx y una base de datos real
- Usa un cliente HTTP async desde el primer día: controla la app en el mismo proceso con `httpx.AsyncClient` sobre `ASGITransport(app=app)`. No uses `async_asgi_testclient`; ya no se mantiene.
- Sobrescribe las dependencias con el mecanismo integrado del framework `app.dependency_overrides[dep] = fake` en lugar de hacer monkeypatching de los internos, y limpia las sobrescrituras después de cada prueba.
- Usa una base de datos real para las pruebas de integración (un esquema efímero o un contenedor desechable) y sobrescribe solo la autenticación y los servicios externos. Simular la base de datos oculta divergencias entre el mock y producción que terminan manifestándose en producción.
## Haz migraciones reversibles y documenta cada endpoint de la API
- Migraciones: mantén cada migración estática y reversible, genéralas a partir de la plantilla async, y usa nombres de archivo descriptivos con marca de tiempo (un prefijo de fecha más un slug) para que el historial se lea en orden.
- Documentación de la API: oculta la documentación interactiva fuera de los entornos elegidos estableciendo la URL de OpenAPI en `None` cuando el entorno actual no esté en una lista de permitidos. Documenta cada endpoint por completo: modelo de respuesta, código de estado, resumen, descripción, etiquetas y una entrada de respuesta por cada estado de error.
- Linting: ejecuta el formateador y el linter (por ejemplo, `ruff check --fix` y luego `ruff format`) e intégralos en un hook de pre-commit o en el CI. Una sola herramienta moderna reemplaza al formateador, al ordenador de imports y a las herramientas de código muerto por separado.
## Detecta y corrige errores comunes de la IA en un diff de FastAPI
Al revisar un diff, señala estos errores de agente; cada fila es un modo de fallo real con su solución.
| Antipatrón | Por qué está mal | Solución |
|---|---|---|
| `requests.get(...)` dentro de `async def` | Bloquea el bucle de eventos | `httpx.AsyncClient`, o `await run_in_threadpool(...)` |
| `time.sleep` / `open()` / driver de BD sync en `async def` | Bloquea el bucle | Equivalente async (`asyncio.sleep`, `aiofiles`, driver async) |
| `from jose import jwt` | Librería sin mantenimiento | `import jwt` (PyJWT) |
| `async_asgi_testclient` | Sin mantenimiento | `httpx.AsyncClient` más `ASGITransport` |
| `ConfigDict(json_encoders=...)` | Obsoleto en Pydantic v2 | `@field_serializer` o `PlainSerializer` |
| `Field(ge=18, default=None)` | La restricción contradice el valor por defecto | Elige obligatorio u opcional, no ambos |
| `Depends(...)` como argumento por defecto | Trampas de valores por defecto propias del legado | `Annotated[T, Depends(...)]` |
| Capturar `Exception` a secas alrededor del cuerpo de una ruta | Oculta bugs, convierte 500 en 200 silenciosos | Captura la clase específica, lanza un error HTTP explícito |
| Tareas en segundo plano para trabajo que requiere aviso urgente | Sin reintento, muere con el worker | Celery / Arq / RQ |
| Sesión ORM sync dentro de `async def` | Bloquea el bucle, puede provocar un interbloqueo en el pool | `AsyncSession` |
| Devolver un modelo y a la vez fijarlo como `response_model` | El modelo se construye dos veces | Devuelve un dict o una fila, o elimina `response_model` |
| Imports profundos entre dominios | Acoplamiento fuerte | Importa el módulo del dominio y ponle un alias |
| Un único `BaseSettings` para toda la app | Cada dominio lee todas las variables | Un `BaseSettings` por dominio |
| Simular la base de datos en pruebas de integración | Divergencia entre el mock y producción | Una BD real más sobrescritura de dependencias para autenticación y servicios externos |
