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)¶
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 imagenpython: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 comoroot.
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)¶
Verificar:
Bajar todo:
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)¶
Lint y formato¶
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.