Skip to content

T1 — Base del proyecto (esqueleto ejecutable)

Qué es esto

La base mínima de DashboardNheo: un servicio FastAPI que arranca con un solo comando, con un endpoint GET /health que confirma que la app y la base de datos están vivas. No tiene features de negocio — es la plataforma sobre la que se construye todo lo demás.

Qué se hizo y por qué

1. Estructura de carpetas por capas

app/
  api/            rutas HTTP (FastAPI routers)
  services/       lógica de negocio
  repositories/   acceso a datos (queries, ORM)
  models/         modelos SQLAlchemy
  workers/        tareas en background / jobs
alembic/          migraciones de base de datos

Por qué así: separar rutas, lógica y acceso a datos evita que el negocio termine mezclado con FastAPI o con SQL crudo. Cuando el proyecto crezca, cada capa se testea y se reemplaza de forma independiente — por ejemplo, cambiar de Postgres a otro motor solo debería tocar repositories/, no services/ ni api/.

2. pyproject.toml como única fuente de dependencias

Se eligió: - FastAPI — framework web, async, tipado con Pydantic. - SQLAlchemy 2.0 — ORM, sintaxis moderna (DeclarativeBase, Mapped). - Alembic — migraciones versionadas de esquema, integrado con SQLAlchemy. - pydantic-settings — configuración validada y tipada desde variables de entorno. - psycopg (v3) — driver de Postgres, reemplazo moderno de psycopg2. - ruff (dev) — lint + formato en una sola herramienta, reemplaza flake8 + black + isort.

Por qué pyproject.toml y no requirements.txt: es el estándar actual de Python (PEP 621), permite declarar dependencias de dev por separado ([project.optional-dependencies]) y sirve como metadata del paquete (hatchling como build backend).

3. Configuración por variables de entorno (app/config.py)

class Settings(BaseSettings):
    database_url: str
    app_env: str = "development"

pydantic-settings lee de variables de entorno o de un archivo .env, y valida los tipos al arrancar — si falta DATABASE_URL, la app no levanta, en vez de fallar más tarde con un error críptico.

Por qué nada hardcodeado: para que el mismo código corra igual en local, en CI y en producción, cambiando solo las variables de entorno. .env está en .gitignore; .env.example documenta qué variables existen sin exponer secretos.

4. GET /health (app/api/health.py)

Ejecuta SELECT 1 contra la base de datos usando una sesión real de SQLAlchemy. Si la query falla, responde 503; si todo está bien, 200 {"status": "ok"}.

Por qué verifica la BD y no solo "la app responde": un health check que solo confirma que el proceso está vivo es inútil para detectar el caso más común en producción — la app arriba pero sin poder hablar con Postgres (credenciales, red, BD caída). Este endpoint es el que usarán los orquestadores (Docker healthcheck, Kubernetes, load balancers) para saber si el servicio está realmente listo.

5. docker-compose.yml — Postgres 15 + app

Dos servicios: db (Postgres 15 con volumen persistente y healthcheck vía pg_isready) y app (build local del Dockerfile). app tiene depends_on: db: condition: service_healthy, así que nunca arranca antes de que Postgres esté realmente aceptando conexiones (no solo "el contenedor existe").

Por qué: que cualquiera en el equipo tenga una base de datos idéntica sin instalar Postgres localmente, y que el orden de arranque sea determinístico.

6. Dockerfile multi-stage, usuario no-root

  • Stage builder: instala dependencias con pip en una imagen python:3.12-slim.
  • Stage runtime: copia solo el resultado (/deps → site-packages) y el código de la app, sin el toolchain de build.
  • Corre como appuser (usuario sin privilegios), no como root.

Por qué multi-stage: la imagen final no arrastra compiladores ni cache de pip, queda más chica y con menos superficie de ataque. Por qué no-root: si alguien compromete el contenedor, no tiene privilegios de root dentro de él — buena práctica estándar de seguridad en contenedores productivos.

7. Alembic configurado (sin migraciones todavía)

alembic.ini + alembic/env.py ya apuntan a Settings.database_url y a Base.metadata (app/models/base.py). No hay modelos todavía porque T1 no define features — pero el próximo modelo que se cree ya tiene el camino de migración listo (alembic revision --autogenerate).

8. ruff + pre-commit

ruff hace lint y formato (reglas E, F, I, UP, B: errores, imports desordenados, sintaxis vieja, bugs comunes). .pre-commit-config.yaml corre ruff --fix y ruff format antes de cada commit, para que el estilo nunca dependa de que alguien se acuerde de correrlo a mano.

9. Repo en GitHub

Push hecho a main en github.com/juanespaez/DashboardNheo. Falta configurar branch protection en main (requerir PR + checks antes de mergear) — pendiente porque necesita gh CLI autenticado o hacerlo manual desde GitHub Settings → Branches. No bloquea el resto del trabajo.

Cómo se corre todo

Con Docker (recomendado, no requiere Python instalado)

cp .env.example .env
docker compose up --build

Verificar:

curl http://localhost:8000/health
# {"status":"ok"}

Bajar todo:

docker compose down

Sin Docker (desarrollo local con Python)

pip install -e ".[dev]"
pre-commit install
cp .env.example .env   # editar DATABASE_URL apuntando a un Postgres local
uvicorn app.main:app --reload

Migraciones (cuando existan modelos)

alembic revision --autogenerate -m "mensaje"
alembic upgrade head

Lint y formato

ruff check .
ruff format .

Cómo se verificó

Se levantó el stack completo con docker compose up --build, se confirmó que el contenedor db pasa su healthcheck antes de que app arranque, y se hizo curl http://localhost:8000/health obteniendo 200 {"status":"ok"} con la base de datos real respondiendo. Después se bajó el stack (docker compose down) y se commiteó el código verificado.