Apéndice E — E. Estructura de proyecto ×3
El árbol de carpetas de Nexus comentado, por lenguaje
La plantilla reusable: dónde vive cada capa (rutas, schemas, servicios, queries, middleware) en Node/TS, Python y Go. Mismos huesos, distinta sintaxis de carpetas — copia el árbol de tu stack y empieza.
E.1 TypeScript (Express)
nexus-api/
├── src/
│ ├── routes/ # HTTP edge: parse → call service → format response
│ │ ├── tasks.ts
│ │ └── auth.ts
│ ├── schemas/ # zod: the contract of each endpoint (input + output)
│ │ └── task.ts
│ ├── services/ # business rules — no req/res here, ever
│ │ └── taskService.ts
│ ├── db/
│ │ ├── pool.ts # THE pool — created once, imported everywhere
│ │ └── queries.ts # parametrized SQL lives here, not in services
│ ├── middleware/
│ │ ├── auth.ts # requireUser — cookie → user → res.locals
│ │ └── errors.ts # THE global handler — DomainError → {error:{code}}
│ ├── errors.ts # DomainError hierarchy
│ └── app.ts # wiring: middleware → routes → error handler LAST
├── migrations/ # dbmate: numbered, versioned, never edited
├── tests/ # vitest + supertest, DB real, rollback-per-test
├── .env.example # every required key documented — the real .env is gitignored
├── Dockerfile # multi-stage: deps → build → runtime
└── compose.yaml # app + postgres for dev
E.2 Python (FastAPI)
nexus-api/
├── app/
│ ├── routers/ # APIRouter per resource: the HTTP edge
│ │ ├── tasks.py
│ │ └── auth.py
│ ├── schemas/ # pydantic: contract IS the framework's language
│ │ └── task.py
│ ├── services/ # business rules — never sees Request/Response
│ │ └── task_service.py
│ ├── db/
│ │ ├── pool.py # connection pool — built at startup
│ │ └── queries.py # parametrized SQL
│ ├── deps.py # Depends: current_user, get_db — injection lives here
│ ├── errors.py # exception hierarchy + handlers
│ └── main.py # wiring: include_router + exception handlers
├── migrations/ # alembic: versions/, env.py
├── tests/ # pytest + TestClient + rollback fixture
├── .env.example
├── Dockerfile
└── compose.yaml
E.3 Go
nexus-api/
├── cmd/
│ └── api/
│ └── main.go # THE entrypoint: config → pool → mux → server
├── internal/ # unimportable from outside — enforced by the compiler
│ ├── http/
│ │ ├── routes.go # ServeMux patterns: "GET /tasks/{id}"
│ │ └── middleware.go # requireUser wrapping http.Handler
│ ├── tasks/
│ │ ├── service.go # business rules
│ │ └── queries.go # parametrized SQL
│ ├── auth/
│ └── errors/ # sentinel errors + the one mapper
├── migrations/ # goose: .sql files, up/down
├── tests/ or *_test.go # go test + httptest, tx rollback
├── .env.example
├── Dockerfile # multi-stage: builder → scratch/distroless binary
└── compose.yaml
E.4 Cómo leer los árboles
Las capas son las mismas, los nombres cambian. routes/routers/ http = el borde HTTP. schemas/schemas/structs en http = el contrato. services = las reglas. db/queries = el SQL. La arquitectura del cap. 7 no es “de Express” — es de backend.
Una query es la pregunta que le haces a la base de datos en SQL: SELECT * FROM tasks WHERE done = false. La BD traduce la pregunta a un plan de búsqueda — por eso los índices importan.
El SQL tiene casa propia. En los tres, las queries viven en un archivo/capa aparte (queries.ts, queries.py, queries.go) — no esparcidas por los servicios. Cuando el DBA pregunta “¿qué le pide tu app a la BD?”, la respuesta es un archivo, no un grep.
Una base de datos es el programa que guarda datos de forma permanente y los responde rápido. Relacional (Postgres): tablas con relaciones. No relacional: documentos, clave-valor, grafos — cada una para una forma de dato distinta.
internal/ en Go es el único muro real. El compilador prohíbe importarlo desde fuera — la privacidad que en los otros stacks es convención, en Go es ley.
El entrypoint es delgado en los tres. app.ts/main.py/main.go solo conectan piezas — si tiene lógica de negocio, el árbol falló.
E.5 Errores comunes
| Error | Por qué pasa | Fix |
|---|---|---|
| Lógica en las rutas | “Es un endpoint pequeño” | La ruta parsea y delega — el servicio decide |
| SQL esparcido | Queries dentro de servicios | queries.* por dominio — un archivo por fuente de verdad |
| El pool recreado por request | createPool() dentro del handler |
El pool nace una vez en el entrypoint y se inyecta |
| Carpetas por tipo técnico | controllers/, models/ genéricos |
Por dominio (tasks/, auth/) — el proyecto crece por recursos |
| Tests en otra galaxia | tests/ sin espejo del src |
El árbol de tests refleja el de src — encontrar el test = saber dónde |
E.6 Mini reto
Arrancar un proyecto nuevo desde cero usando solo el árbol como guía.
El orden correcto: entrypoint delgado → pool inyectado → una ruta de ejemplo que recorre las 4 capas (ruta → schema → servicio → query) → error handler → un test de integración. Si en 30 minutos tienes GET /health + un endpoint CRUD completo con las capas separadas, el árbol funciona — y ese esqueleto es literalmente lo que un lead espera el día 1.
E.7 Vocabulario técnico del capítulo
Un bucle repite una acción por cada elemento o hasta cumplir una condición: for task in tasks hace algo con cada tarea. map y filter son bucles disfrazados de funciones — transforman/filtran colecciones sin for explícito.
La autenticación responde “¿quién eres?” — login, contraseña, token. Se confunde con autorización (“¿qué puedes hacer?”), que es la pregunta siguiente. Primero te identificas, luego te dejan o no pasar.
Una cookie es un dato que el servidor pone en tu navegador y el navegador devuelve en cada request. httpOnly = JavaScript no puede leerla (el XSS no la roba); Secure = solo viaja por HTTPS.
Un middleware es un filtro en la cadena del request: pasa por él antes de llegar a tu handler. Auth es middleware — “verifica el token” vive una vez y protege todas las rutas, no se copia en cada una.
Una variable de entorno es configuración que vive fuera del código: DATABASE_URL, JWT_SECRET. El mismo binario corre en dev y prod con distintas vars — los secretos nunca se escriben en el código.
Un servidor es una computadora que espera peticiones y las responde 24/7. Físicamente no es nada mágico: es una máquina (a veces una VM alquilada) corriendo tu programa, con la diferencia de que está siempre encendida y conectada.
Un dominio es el nombre que compras (midominio.com) y apuntas a tu servidor por DNS. Sin él, tus usuarios tendrían que memorizar una IP. El HTTPS serio requiere dominio.
Docker Compose describe un sistema de varios contenedores en un YAML: la app + Postgres + Redis, sus redes y volúmenes. docker compose up levanta el entorno completo de desarrollo con un comando.
Un rollback es volver atrás: la migración se revierte, el deploy regresa a la versión anterior. Todo cambio serio tiene plan de rollback antes de ejecutarse — si no, el plan es rezar.
Un health check es el endpoint /health donde la app dice “estoy viva”. El orquestador/load balancer lo consulta cada pocos segundos: si falla, reinicia la instancia o deja de mandarle tráfico.
El runtime es el motor que ejecuta tu código: Node.js es el runtime de JavaScript fuera del navegador; CPython es el de Python; Go compila a binario y el runtime va empaquetado dentro. Es “quien corre” lo que escribiste.
Un framework es un esqueleto de aplicación ya decidido: te da la estructura (rutas, validación, errores) y tú llenas la lógica. Diferencia con librería: la librería la llamas tú; el framework te llama a ti.
El build (construcción) es el proceso que convierte tu código fuente en algo ejecutable/entregable: compilar TypeScript a JS, empaquetar el frontend, armar la imagen Docker. Lo que se despliega es el resultado del build, no tu código crudo.
Un compilador traduce código a otra forma: TypeScript → JavaScript (transpila), Go → binario (compila). Atrapa errores antes de ejecutar. tsc, esbuild, go build son compiladores.
La memoria RAM es el escritorio de trabajo del programa: rápida, pero se borra al apagar. Los datos que deben sobrevivir van a disco o a la base de datos. Por eso let tasks = [] pierde todo al reiniciar.
El esquema es el plano de la base de datos: qué tablas hay, qué columnas tiene cada una y de qué tipo. Es contrato: una fila que no cumple el esquema no entra. Se cambia con migraciones, no a mano.
Una transacción es un grupo de operaciones que se confirman juntas o no se confirma ninguna: transferir dinero = restar de A y sumar a B. Si falla a la mitad sin transacción, el dinero desapareció.
Un pool es el staff de conexiones a la BD: abrir una conexión por request es caro, así que el pool mantiene ~10–20 abiertas y las presta. El request la usa, la devuelve, y la siguiente la reutiliza.