0005. Código limpio para Python con FastAPI¶
Estado: Propuesto Fecha: 2026-08-28 Autor: Duval Alcivar
Contexto¶
El backend se desarrolla en Python con FastAPI siguiendo el patrón de capas Router → Service → Repository (ADR 0001). Hay que fijar cómo se escribe el código dentro de cada servicio para que sea consistente entre los 14 microservicios y siga siendo testeable, legible y auditable a medida que el equipo crece.
Sin convenciones, cada servicio termina con estilos distintos: validaciones dentro del router, SQL en el service, modelos gigantes, y proyectos que tardan en que un nuevo desarrollador contribuya.
Decisión¶
Todo el código FastAPI del proyecto sigue las siguientes convenciones.
Estructura de un servicio¶
- Un paquete por dominio de negocio, con sus cuatro responsabilidades
separadas en módulos:
router.py(HTTP),service.py(reglas de negocio),repository.py(acceso a datos),schemas.py(Pydantic) ymodels.py(ORM/SQLAlchemy). - Nada de lógica de negocio en
router.py; nada de SQL enservice.py; nada de HTTP fuera derouter.py(ADR 0001). - TODO, marcadores y tipos genéricos (
Any,object) documentados o eliminados en revisión.
Nombrado y estilos¶
- Seguir PEP 8 y la guía de estilo oficial del proyecto (ruff/black):
funciones y variables en
snake_case, clases enPascalCase, constantes enUPPER_SNAKE_CASE. - Nombres expresivos:
crear_pedido()y noprocesar();stock_actualy nos. - Tipado estricto en firmas: nada de funciones sin anotación de tipo.
- Docstrings en los módulos y en las funciones públicas del
service(reglas de negocio no obvias).
FastAPI¶
- Los endpoints son delgados: validan el esquema con Pydantic, chequean permisos y delegan al service. Sin excepción.
Dependspara inyección de dependencias (sesión de DB, empresa activa, usuario actual) — nunca construir esas dependencias dentro del handler. En particular,get_empresa_actualse inyecta solo en las rutas que escriben (crear/actualizar/borrar); las rutas de lectura no lo usan porque RLS ya filtra solo (ADR 0012).- Errores de dominio se modelan como excepciones propias del servicio y se
traducen a HTTP en un handler global, no con
try/exceptdesperdigado en los routers. - Async solo donde da beneficio real (I/O). No mezclar estilos en el mismo módulo.
Pydantic y validación¶
- Un
schemas.pypor dominio con modelos de entrada (*Request), salida (*Response) e interno (*DTO), sin reusar el modelo ORM para la salida. - Usar
model_config = ConfigDict(from_attributes=True)para serializar desde ORM a los DTO de salida. - Validación de negocio en el service; validación de forma/formato en Pydantic.
- Nada de
**kwargspara guardar entradas sin esquema definido.
Repositorios y ORM¶
- Solo el
repositoryimporta SQLAlchemy/PostgreSQL (ADR 0001). - Consultas parametrizadas vía ORM; nunca concatenación de SQL con datos del usuario.
- Las sesiones se abren y cierran por request (inyectadas con
Depends), nunca abiertas a mano dentro de la lógica de negocio. - El aislamiento por empresa lo garantiza RLS + el middleware (ADR 0012):
el middleware setea
app.current_empresa_iduna vez por request. Por eso los repositorios no inyectan ni filtran porempresa_iden cada query (no escribenWHERE empresa_idniSET); RLS filtra automáticamente. Esto es coherente con el ADR 0002 y con la eliminación delWHERE co_empresamanual.
Tests¶
- Unidad sobre
servicecon repositorio mockeado — sin base de datos real. - Integración sobre
repositorycontra PostgreSQL real en CI. contract testcontra el contrato OpenAPI de cada servicio (ADR 0003).test de aislamiento por empresaen cada servicio (ADR 0002).- Nombre de tests descriptivo:
test_crear_pedido_falla_sin_stock.
Dependencias y calidad¶
- Dependencias declaradas explícitamente en
pyproject.toml(Poetry), con versiones fijadas en el lockfile (poetry.lock) — nunca requirements sueltos y sin pinear. - CI corre
ruff,mypy(strict) ypytesten cada PR; cualquier aviso bloquea el merge. - Un PR se rechaza si mezcla responsabilidades entre capas o introduce un patrón nuevo sin su ADR.
Nota: qué es un venv, y cómo se usa (no es algo de FastAPI).
Primero aclaremos la confusión: venv no compite con pyproject.toml ni
con requirements.txt — son cosas distintas que se usan a la vez.
El venv (virtual environment) es una carpeta aislada (por convención
.venv/) dentro de tu proyecto, con su propia copia de Python y de los
paquetes instalados. Cuando lo activas, la consola usa ese Python y esos
paquetes, no los globales de tu sistema. Sirve para que los proyectos A y B
no se pisen entre sí (el A pide Django 4, el B pide Django 5, y ambos
coexisten sin romperse). Esto aplica a cualquier proyecto Python, no
solo a FastAPI.
El pyproject.toml / requirements.txt, en cambio, es la lista de qué
instalar dentro de ese venv. El venv es el contenedor aislado donde se
instala.
Escondiéndolo del control de versiones: el .venv/ es específico de la
máquina y no se commitea — se add al .gitignore; cada desarrollador crea
el suyo con el mismo pyproject.toml.
Crear y activar el venv (por sistema):
# 1) CREAR (una sola vez, igual en los 3 SO)
python -m venv .venv
# 2) ACTIVAR — Windows / PowerShell
.venv\Scripts\Activate.ps1
# 2) ACTIVAR — Windows / CMD
.venv\Scripts\activate.bat
# 2) ACTIVAR — Linux / macOS (bash, zsh)
source .venv/bin/activate
Después de activar ya instalas y corres con python/pip apuntando al
venv. Para desactivarlo: escribe deactivate (igual en los tres).
Por qué "activar": al activarse, la variable PATH de la consola se
modifica para que python y pip sean los del .venv. Así pip install
deja paquetes dentro de la carpeta y no en el sistema.
Si no quieres activar (opcional): en vez de activar, llama directo a la
rutina. Es lo mismo, solo que sin tocar el PATH:
# Windows # Linux / macOS
.venv\Scripts\python.exe .venv/bin/python
.venv\Scripts\pip.exe install ... .venv/bin/pip install ...
Y entonces, para las librerías:
requirements.txt— lista plana (fastapi==0.115.0). Fija solo las versiones directas; las dependencias secundarias quedan sueltas y varían de una instalación a otra. Aceptable para scripts sueltos, insuficiente para reproducir dev/staging/producción idénticos.pyproject.toml— estándar moderno (PEP 518/621): describe el proyecto (nombre, versión, dependencias de ejecución vs desarrollo, y la configuración de ruff/mypy/pytest/black/poetry) y se combina con un lockfile que fija también las secundarias. Instalación reproducible en los 14 servicios.
Flujo de referencia con Poetry (el gestor estándar del proyecto, que además te crea y activa el venv solo):
poetry new svc-ejemplo # crea el proyecto (o `poetry init` en uno existente)
poetry add fastapi sqlalchemy # declara en pyproject.toml y fija poetry.lock
poetry run ... # activa el venv, instala y corre, en un paso
Resumen: venv = contenedor aislado (siempre se usa, se activa con el comando de tu SO); pyproject/lockfile = qué instalar, reproducido igual en todos lados.
Ponte en marcha: setup de un microservicio FastAPI (paso a paso)¶
Todo lo anterior en una guía práctica, del cero a correr el servicio con sus migraciones. Regla de oro: todo se instala y corre dentro del venv del proyecto, nunca en el Python del sistema, para no afectar a otros proyectos Python/FastAPI.
1. Instalar Python¶
- Descargar de https://www.python.org/downloads/ (versión LTS en uso por el equipo, p. ej. 3.12/3.13).
- Windows: marcar "Add python.exe to PATH" durante la instalación.
Verificar en terminal:
python --versionypip --version. - Linux/macOS: usar el gestor de paquetes del sistema o
pyenvpara fijar la versión por proyecto. Verificar igual conpython --version. - Poetry (https://python-poetry.org/) como gestor de dependencias
estándar del proyecto — gestiona el venv, las dependencias y el lockfile
(
poetry.lock) de forma reproducible en un solo comando. Instalación:pipx install poetry(opip install --user poetry).
2. Crear el proyecto y su entorno aislado¶
mkdir svc-operaciones && cd svc-operaciones # nombre del microservicio
poetry init # crea pyproject.toml (o `poetry new` desde cero)
Poetry crea y gestiona el .venv automáticamente en el primer poetry install:
no hace falta python -m venv ni activarlo a mano. Para forzar el intérprete:
poetry env use python. Para entrar al entorno: poetry shell.
Crear pyproject.toml (o dejarlo que lo genere poetry init). Mínimo:
[tool.poetry]
name = "svc-operaciones"
version = "0.1.0"
description = "Microservicio de operaciones"
authors = ["<equipo>"]
packages = [{ include = "app" }]
[tool.poetry.dependencies]
python = "^3.12"
fastapi = "^0.115.0"
uvicorn = { version = "^0.32.0", extras = ["standard"] }
sqlalchemy = "^2.0.35"
psycopg2-binary = "^2.9.0"
alembic = "^1.14.0"
pydantic-settings = "^2.5.0"
[tool.poetry.group.dev.dependencies]
ruff = "^0.6.0"
mypy = "^1.11.0"
pytest = "^8.3.0"
httpx = "^0.27.2"
3. Instalar las dependencias (siempre dentro del venv)¶
poetry install # crea el venv, instala runtime + dev y fija poetry.lock
4. Estructura del microservicio (según las convenciones del ADR 0001)¶
svc-operaciones/
├── .venv/ # NO se commitea → al .gitignore
├── pyproject.toml
├── alembic.ini
├── alembic/ # migraciones Alembic versionadas
│ ├── versions/ # una migración por cambio
│ └── env.py
├── app/
│ ├── main.py # crea la app FastAPI, monta routers
│ ├── core/ # config, seguridad, excepciones, logging
│ │ └── config.py # pydantic-settings: lee variables de entorno
│ ├── api/
│ │ └── deps.py # get_db(), get_current_user() — transversal
│ ├── domains/ # un dominio por carpeta (vertical slice)
│ │ ├── dominio1/
│ │ │ ├── router.py # HTTP (delgado)
│ │ │ ├── service.py # reglas de negocio
│ │ │ ├── repository.py # ORM/SQLAlchemy (único que toca la DB)
│ │ │ ├── schemas.py # Pydantic: *Request / *Response / *DTO
│ │ │ └── models.py # modelos ORM
│ │ └── dominio2/...
│ ├── shared/ # solo lo que cruza 2+ dominios de ESTE servicio
│ └── events/ # outbox: publisher/subscriber
└── tests/ # pytest: unit/ e integration/, por dominio
5. Migraciones con Alembic¶
Alembic versiona cada cambio de esquema (ADR 0004) y es la herramienta que migra cada servicio sobre su propia base/esquema:
alembic init alembic # la 1ª vez: crea alembic.ini + alembic/
alembic revision --autogenerate -m "crear tabla mae_activos" # crea la migración
Aplicar las migraciones pendientes:
alembic upgrade head # aplica TODAS las pendientes hasta la última revisión
alembic upgrade +1 # solo la siguiente
alembic upgrade <revision> # hasta una revisión concreta
Revisar estado y revertir:
alembic current # revisión aplicada actualmente
alembic history # traza quién/cuándo/por qué
alembic downgrade -1 # revierte el último cambio (rollback)
alembic downgrade base # revierte todo
Invocación según el entorno (desde la carpeta con alembic.ini, o con
-c ruta/alembic.ini):
alembic upgrade head # con el venv activado
poetry run alembic upgrade head # con Poetry (canónico)
python -m alembic upgrade head # si alembic no está en el PATH
La cadena de conexión se define en app/core/config.py vía variables de
entorno (DATABASE_URL), nunca hardcodeada. El esquema lo migra solo el
pipeline de su propio servicio.
6. Correr el proyecto (dev)¶
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
# --reload recarga al guardar; docs interactivas en http://localhost:8000/docs
Con Poetry y todo en un comando: poetry run uvicorn app.main:app --reload.
7. Chequeos antes de merge (CI)¶
ruff check . # lint
ruff format . # formato
mypy app # tipado estricto
pytest # tests (unidad con mocks, integración, contract, aislamiento)
Recomendaciones¶
- Todo dentro del venv: si te aparece "pip no es un comando" o "ModuleNotFoundError", es que no está activo el venv — actívalo primero.
- Nunca
pip installglobal fuera del proyecto: contamina el Python del sistema y rompe el aislamiento entre microservicios. - Fijar versiones: paquetes declarados en
pyproject.toml+ lockfile (poetry.lock), no sueltos ni sin pinear. - Poetry como estándar: hace venv + instalar + lockfile + correr en menos
pasos y sin la molesta gestión del
PATHpor SO.
Documentación oficial¶
- Python + entornos virtuales: https://docs.python.org/3/library/venv.html y https://docs.python.org/3/tutorial/venv.html
- Guía de empaquetado (por qué
pyproject.toml, PEP 518/621): https://packaging.python.org/en/latest/tutorials/packaging-projects/ - FastAPI — oficial, incluye estructura de proyecto: https://fastapi.tiangolo.com/tutorial/
- FastAPI — estilo y dependencias: https://fastapi.tiangolo.com/
- SQLAlchemy: https://docs.sqlalchemy.org/ , Alembic: https://alembic.sqlalchemy.org/
- Pydantic +
pydantic-settings: https://docs.pydantic.dev/ - Poetry: https://python-poetry.org/ | Ruff: https://docs.astral.sh/ruff/ | Mypy: https://mypy.readthedocs.io/ | Pytest: https://docs.pytest.org/
Alternativas descartadas¶
- FastAPI con lógica en los routers (estilo actual del legado) — descartado: reproduce la causa raíz de los problemas de testeo y auditoría que motivan la migración (ADR 0001).
- Arquitectura hexagonal / DDD completo con puertos e interfaces en cada servicio — descartada por ahora por consistencia con el ADR 0001: agrega abstracciones que el equipo no necesita aún; puede evolucionarse por dominio si uno lo justifica.
- NestJS como framework alternativos — descartado para los servicios nuevos: el equipo ya tiene base en Python y FastAPI sobre el core actual de PostgreSQL.
Consecuencias¶
- Código consistente y testeable en los 14 servicios, con coste de entrada bajo para nuevos desarrolladores.
- Exige disciplina de revisión: PRs que incumplan estas convenciones
(capas mezcladas, tipado suelto,
try/excepten routers) se rechazan. - El CI pasa a ser el guardián: ruff + mypy strict + pytest en cada PR.
- Queda fuera de este ADR el patrón concreto de outbox/mensajería entre servicios, que se define en el ADR de mensajería y contratos.