Saltar a contenido

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) y models.py (ORM/SQLAlchemy).
  • Nada de lógica de negocio en router.py; nada de SQL en service.py; nada de HTTP fuera de router.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 en PascalCase, constantes en UPPER_SNAKE_CASE.
  • Nombres expresivos: crear_pedido() y no procesar(); stock_actual y no s.
  • 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.
  • Depends para inyección de dependencias (sesión de DB, empresa activa, usuario actual) — nunca construir esas dependencias dentro del handler. En particular, get_empresa_actual se 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/except desperdigado 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.py por 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 **kwargs para guardar entradas sin esquema definido.

Repositorios y ORM

  • Solo el repository importa 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_id una vez por request. Por eso los repositorios no inyectan ni filtran por empresa_id en cada query (no escriben WHERE empresa_id ni SET); RLS filtra automáticamente. Esto es coherente con el ADR 0002 y con la eliminación del WHERE co_empresa manual.

Tests

  • Unidad sobre service con repositorio mockeado — sin base de datos real.
  • Integración sobre repository contra PostgreSQL real en CI.
  • contract test contra el contrato OpenAPI de cada servicio (ADR 0003).
  • test de aislamiento por empresa en 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) y pytest en 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 --version y pip --version.
  • Linux/macOS: usar el gestor de paquetes del sistema o pyenv para fijar la versión por proyecto. Verificar igual con python --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 (o pip 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 install global 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 PATH por 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/except en 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.