Express Clean Backend
Template Node.js/TypeScript de producción: arquitectura hexagonal estricta, Keycloak OIDC, CQRS-lite, S3, Traefik 3.7 con ACME y stack Loki/Grafana/Prometheus.
Node.js · TypeScript · Hexagonal Architecture · Clean Architecture

Visión general
Backend Node 22 + TypeScript pensado como punto de partida para una API en producción. No es un boilerplate de "Hello World con autenticación": viene con IdP delegado a Keycloak, CQRS-lite a nivel de repositorios, auditoría desacoplada por eventos, storage S3 detrás de un port, y stack completo de logs y métricas tras Traefik con HTTPS automático.
La API está desplegada en api.jmrg.dev/api-docs y el endpoint de readiness en api.jmrg.dev/health/ready.
Motivación
Cada vez que arrancaba un proyecto Node desde cero acababa copiando los mismos cuatro módulos: el wrapper de Keycloak, el logger con correlación, el error handler con su chain, y el docker-compose con Traefik. Este template consolida todo eso en un repo con arquitectura hexagonal estricta y reglas verificables, así esas decisiones no se vuelven a tomar a mano en cada proyecto nuevo.
Decisiones que vale la pena explicar
Keycloak como IdP único. Cero JWT propietarios. La API valida RS256 contra el JWKS con jose. Los paneles humanos (Grafana, Traefik, Prometheus) se protegen con el plugin OIDC de Traefik apuntando al mismo realm, así no hay tres logins distintos para tres servicios.
Repositorios separados para lectura y escritura. UserQueryRepository y UserCommandRepository no comparten interfaz. Evita que un findById acabe arrastrando métodos de mutación y deja la puerta abierta a escalar a CQRS real cuando haga falta, sin reescribir el dominio.
Auditoría vía Observer. LoginUseCase publica eventos en un EventBusPort y AuditLoginObserver los persiste. El caso de uso no sabe que la auditoría existe.
Storage detrás de un port. StoragePort con S3StorageAdapter para AWS real (o LocalStack en dev) y FakeStorageAdapter para tests. El adapter incluye guard contra path-traversal porque alguien lo va a intentar.
Logging con contexto. Winston decorado con RequestContextLoggerDecorator que propaga el correlationId por AsyncLocalStorage. No hay que pasarlo por parámetro en cada función.
Observabilidad provisionada. Loki + Prometheus + Grafana con dashboards y datasources en JSON. Levanta y funciona, sin clickar dashboards a mano.
HTTPS automático. Traefik 3.7 emite el certificado vía Let's Encrypt en el primer arranque con HTTP-01 challenge y se encarga de renovarlo.
Arquitectura
Cuatro capas con dependencias dirigidas hacia el centro:
src/
├── domain/ # entidades, value objects, ports
├── application/ # use cases por feature
├── infrastructure/ # adapters: keycloak, mongo, s3, winston, events
└── presentation/ # routers Express + middlewares + bootstrapLa regla es simple: nada dentro de domain/ ni application/ puede importar mongoose, express, aws-sdk, keycloak-connect, jose ni winston. ESLint lo verifica en CI.
Stack
| Capa | Tecnología |
|---|---|
| Runtime | Node ≥ 22, TypeScript strict, ESM nativo |
| HTTP | Express 5 + helmet, cors, express-rate-limit |
| Auth | Keycloak 26.6 (OIDC) + jose (JWKS RS256) |
| Persistencia | MongoDB 8.2 + Mongoose |
| Storage | AWS S3, LocalStack en dev |
| Validación | Zod fail-fast por subsistema |
| Logs | Winston → Loki 3.7 |
| Métricas | Prometheus 3.8 + Grafana 13 |
| Reverse proxy | Traefik 3.7 + plugin OIDC sevensolutions/traefik-oidc-auth |
| Testing | Vitest + supertest + mongodb-memory-server |
| Package manager | pnpm |
Patrones de diseño
| Categoría | Patrón | Dónde aparece |
|---|---|---|
| Estructural | Hexagonal (Ports & Adapters) | domain/<feature>/port ← infrastructure/<feature>/adapter |
| Estructural | Adapter | KeycloakAdapter, S3StorageAdapter, WinstonLoggerAdapter |
| Estructural | Facade | UserFacade, AuthFacade, StorageFacade |
| Estructural | Decorator | RequestContextLoggerDecorator |
| Comportamiento | Chain of Responsibility | Error handler: Zod → Client → Server → Mongo → Fallback |
| Comportamiento | Observer | EventBusPort + AuditLoginObserver |
| Comportamiento | Strategy | Selector de formato en WinstonLoggerAdapter |
| Comportamiento | Template Method | ErrorHandler.handle / delegate |
| Creacional | Factory Method | CustomError.notFound, .conflict, .unauthorized… |
| Creacional | Singleton | MongoDatabase, S3Client |
| DDD | Repository CQRS-lite | UserQueryRepository + UserCommandRepository |
Endpoints
| Método | Endpoint | Auth | Roles |
|---|---|---|---|
| POST | /api/v1/auth/register |
público, rate-limit 3/min/IP | — |
| POST | /api/v1/auth/login |
público, rate-limit 5/min | — |
| POST | /api/v1/auth/refresh |
público | — |
| GET | /api/v1/auth/me |
Bearer | cualquiera |
| GET | /api/v1/users |
Bearer | admin |
| POST | /api/v1/users |
Bearer | admin |
| PUT | /api/v1/users/:id |
Bearer | admin |
| DELETE | /api/v1/users/:id |
Bearer | admin |
| GET | /api/v1/storage/objects |
Bearer | admin |
| GET | /api/v1/storage/signed-url |
Bearer | admin |
| GET | /health/ready |
público | — |
| GET | /metrics |
LAN-only | — |
Spec OpenAPI completo en api.jmrg.dev/api-docs.
Despliegue
En desarrollo, dos comandos:
docker compose --profile dev up -d # base + LocalStack
docker compose --profile dev --profile observability up -d # + Loki/Grafana/Prom
pnpm dev # API en host con tsx watchEn producción se define .env.production:
DOMAIN=tudominio.com
SSL_EMAIL=[email protected]
KEYCLOAK_INTERNAL_URL=https://auth.tudominio.comY se arranca:
./scripts/init-acme.sh
docker compose --env-file .env.production up -dTraefik emite el certificado de Let's Encrypt en el primer arranque y lo renueva automáticamente.
Documentación
14 capítulos en docs/ distribuidos en cuatro bloques:
- Proyecto — estructura, arquitectura, features, seguridad, configuración, testing, contribución.
- API — endpoints, schemas Zod, cadena de errores.
- Infraestructura — Docker, Traefik, ACME, observabilidad.
- Operación — runbook, healthchecks, troubleshooting.
Links
- Repo: github.com/jmrg-link/express_clean_code_template
- API: api.jmrg.dev/api-docs
- Health: api.jmrg.dev/health/ready
- Licencia: MIT