jagg-cli
v0.4.0
Published
CLI de scaffolding con Clean Architecture, adapters y singletons + docker-compose
Downloads
1,048
Maintainers
Readme
jagg-cli
CLI de scaffolding para proyectos Node.js + TypeScript con Clean Architecture. Con un comando genera los archivos de un servicio (contratos de dominio, adapters, singletons), instala sus dependencias, lo agrega al docker-compose.yml y completa el .env.example y el .env (los crea si no existen y nunca pisa valores que ya tengas). Todo se puede deshacer.
npm install -g jagg-cli
jg g clean # estructura de carpetas
jg g express # servidor Express 5 + TypeScript listo para usar
jg g env # validador de variables de entorno (Zod)
jg g redis # cache Redis + servicio docker
jg g prisma # ORM Prisma + Postgres en docker
jg g user-crud # CRUD de usuarios completo en /api/users
jg g datasource user --orm mongoose # cambia de base de datos/ORM sin tocar el dominio
jg g metrics # monitoreo con Prometheus + Grafana, dashboard incluido
jg undo # deshace lo últimoRequisitos
- Node.js 20.12 o superior
- Un proyecto con
package.json(npm, pnpm, yarn o bun; se detecta por el lockfile)
Empezar un proyecto desde cero
mkdir mi-api && cd mi-api
npm init -y # jg necesita un package.json
jg g clean
jg g express
jg g user-crud
npm run dev # http://localhost:3000/api/usersNo hace falta ejecutar npm install a mano: cada generador instala sus propias dependencias con el gestor de paquetes del proyecto (por ejemplo, jg g express instala express, typescript, tsx...). Si usas --skip-install, instálalas tú después con npm install <paquetes> (la columna "Dependencias" de la tabla de generadores dice cuáles). Al clonar un proyecto ya generado, basta el npm install habitual.
Comandos
| Comando | Descripción |
|---|---|
| jg g <generador> [nombre] | Ejecuta un generador (g es alias de generate) |
| jg list | Lista los generadores disponibles |
| jg undo [n] | Deshace las últimas n generaciones (por defecto 1) |
| jg history | Muestra lo que se puede deshacer |
| jg init | Crea jg.config.json |
Opciones de generate
| Opción | Efecto |
|---|---|
| -d, --dry-run | Muestra lo que haría sin escribir nada |
| -f, --force | Sobrescribe archivos y servicios existentes |
| --skip-install | No instala dependencias |
| --skip-docker | No modifica docker-compose.yml |
| --src <dir> | Carpeta base del código (por defecto src) |
Ejecutar dos veces el mismo generador es seguro: los archivos existentes se saltan salvo que uses --force.
Generadores
| Generador | Qué genera | Dependencias | Docker |
|---|---|---|---|
| clean | Estructura de carpetas Clean Architecture | — | — |
| express | Servidor Express 5, tsconfig, scripts dev/build/start, manejo de errores | express, cors, helmet, typescript, tsx, tsup | Dockerfile |
| env | Validador de env estilo env-var con adapter Zod | zod | — |
| redis | Contrato de cache, servicio, cliente singleton | ioredis | redis:7-alpine |
| postgres | Config y Pool singleton | pg | postgres:16-alpine |
| prisma | Schema, cliente singleton, scripts | @prisma/client, @prisma/adapter-pg, prisma | postgres:16-alpine |
| drizzle | Schema, cliente singleton, scripts | drizzle-orm, pg, drizzle-kit | postgres:16-alpine |
| typeorm | DataSource con EntitySchema, singleton, migraciones | typeorm, pg | postgres:16-alpine |
| sequelize | Modelos con Model.init, singleton, sequelize:sync | sequelize, pg, pg-hstore | postgres:16-alpine |
| mongoose | Conexión singleton, modelo User | mongoose | mongo:7 |
| entity <Nombre> | Entidad de dominio | — | — |
| repository <Nombre> | Contrato + implementación in-memory | — | — |
| repository <Nombre> --orm prisma\|drizzle | Contrato + mapper + repositorio con el ORM | — | — |
| usecase <nombre> | Caso de uso | — | — |
| user-crud [--orm <orm>] | CRUD de usuarios: entidad, DTOs, contratos, casos de uso, API (+caché si hay redis) | — | — |
| datasource user --orm <orm> | Cambia el datasource del CRUD sin tocar el dominio | — | — |
| metrics [--path] [--protect] [--no-load-shedding] | Métricas HTTP, de casos de uso y del proceso + dashboard de Grafana + 503 inmediato si se satura | @prometheus-io/client | prom/prometheus, grafana/grafana |
Los ORMs (prisma, drizzle, typeorm, sequelize, mongoose), express y repository --orm requieren la estructura clean; user-crud requiere clean y express; metrics requiere express. Donde aparece <orm> vale prisma, drizzle, typeorm, sequelize, mongoose o memory.
jg g clean
src/
├── domain/api/
│ ├── datasources/ dtos/ repositories/
│ └── entities/ services/ use-cases/
├── infrastructure/
│ ├── api/
│ │ ├── mappers/ datasources/ repositories/
│ │ └── services/ ← implementaciones de servicios (ej. Redis)
│ ├── data/ ← ORMs (Prisma, Drizzle, Postgres)
│ └── redis/ ← config + cliente singleton
└── presentation/El dominio solo contiene contratos (interfaces) para invertir dependencias; la infraestructura los implementa. Una vez creada esta estructura, el resto de los generadores escribe en ella automáticamente.
jg g express
Deja un proyecto nuevo listo para arrancar:
tsconfig.json # estricto, module Preserve + moduleResolution Bundler
tsup.config.ts # build a dist/main.js (ESM)
Dockerfile # multi-stage, usuario no root
.dockerignore .gitignore
src/
├── load-env.ts # carga .env antes que cualquier config
├── main.ts # arranque + cierre ordenado (SIGINT/SIGTERM)
└── presentation/http/
├── app.ts # helmet, cors, JSON, rutas, 404 y errores
├── routes/ # index.ts + health.routes.ts (GET /api/health)
├── errors/ # HttpError.badRequest(), .notFound(), ...
└── middlewares/ # not-found.ts, error-handler.tsAgrega "type": "module" a package.json y los scripts:
| Script | Comando |
|---|---|
| npm run dev | tsx watch src/main.ts (recarga al guardar) |
| npm run build | tsup |
| npm start | node dist/main.js |
| npm run typecheck | tsc --noEmit |
Con jg g env previo, el puerto y NODE_ENV se leen validados desde appConfig. Las rutas lanzan HttpError y el manejador central responde con el status y un JSON { error, details }.
jg g clean && jg g express
npm run dev # http://localhost:3000/api/healthjg g env
import { env } from './infrastructure/env/env.provider';
const port = env.get('PORT').required().asPort(); // number
const debug = env.get('DEBUG').default('false').asBool(); // boolean
const tags = env.get('TAGS').asArray(); // string[] | undefined
const mode = env.get('NODE_ENV').required().asEnum(['development', 'production']);Métodos: asString, asInt, asFloat, asPort, asBool, asUrl, asEmail, asEnum, asArray, asJson<T>(). Sin .required() ni .default() el tipo incluye undefined. Un valor inválido lanza EnvValidationError al arrancar.
Si env existe, redis, postgres y los ORMs validan su configuración con él.
jg g redis
import { getCache } from './infrastructure/api/services/cache.provider';
const cache = getCache(); // CacheService (contrato de dominio)
await cache.set('user:1', { name: 'Ana' }, 60); // TTL en segundos
const user = await cache.get<{ name: string }>('user:1');jg g prisma / jg g drizzle
Crean la configuración en infrastructure/data/, un modelo/tabla User de ejemplo, el servicio postgres en docker y scripts en package.json:
docker compose up -d postgres
npm run prisma:migrate -- --name init # Prisma
npm run drizzle:generate && npm run drizzle:migrate # DrizzleEl cliente de Prisma se genera automáticamente al crear Prisma, el CRUD o al cambiar de datasource. Solo hay que volver a ejecutar npm run prisma:generate si editas schema.prisma a mano.
Luego, un repositorio real:
jg g repository Product --orm prismadomain/api/entities/product.entity.ts # entidad
domain/api/repositories/product.repository.ts # contrato
infrastructure/api/mappers/prisma-product.mapper.ts # toDomain / toPersistence
infrastructure/api/repositories/prisma-product.repository.ts # implementaciónAdemás agrega model Product a schema.prisma (o la tabla products al schema de Drizzle).
jg g typeorm / jg g sequelize / jg g mongoose
Misma idea que Prisma y Drizzle: configuración en infrastructure/data/<orm>/, cliente singleton, modelo User de ejemplo y base de datos en docker. Ninguno usa decoradores (tsx y tsup compilan con esbuild, que no emite metadata), así que el modelo de persistencia queda en infraestructura:
| ORM | Modelo | Tablas / colecciones |
|---|---|---|
| TypeORM 1.x | EntitySchema en entities/user.schema.ts | npm run typeorm:generate && npm run typeorm:migrate (migraciones JS ESM) |
| Sequelize 6 | Model.init en models/user.model.ts | npm run sequelize:sync (desarrollo; en producción, migraciones) |
| Mongoose 9 | Schema en models/user.model.ts | No hacen falta: colección e índice se crean al usarse |
docker compose up -d postgres # TypeORM / Sequelize
docker compose up -d mongo # Mongoose (MONGO_URL en .env)repository --orm sigue soportando solo Prisma y Drizzle; los cinco ORMs funcionan con user-crud y datasource.
jg g user-crud
Genera un CRUD de usuarios (id, name, email único, createdAt, updatedAt) recorriendo todas las capas, y lo registra en /api/users. Conviene ejecutarlo al final, cuando ya están el ORM y Redis:
jg g clean && jg g express && jg g env && jg g redis && jg g prisma
jg g user-cruddomain/api/
├── entities/user.entity.ts # UserEntity (readonly + static create)
├── dtos/user/ # CreateUserDto, UpdateUserDto → [error, dto]
├── datasources/user.datasource.ts # contrato (abstract class)
├── repositories/user.repository.ts # contrato (abstract class)
├── errors/user.errors.ts # UserNotFoundError, EmailAlreadyInUseError
└── use-cases/user/ # create, get-users, get-user-by-id, update, delete
infrastructure/api/
├── datasources/prisma-user.datasource.ts # o drizzle- / typeorm- / sequelize- / mongoose- / in-memory-
├── mappers/prisma-user.mapper.ts # fila del ORM → UserEntity
└── repositories/user.repository.impl.ts # delega en el datasource
presentation/http/user/
├── user.composition.ts # composition root: arma el grafo
├── user.controller.ts # errores de dominio → 404 / 409
└── user.routes.ts- Datasource: el ORM configurado; si hay varios, elige con
--orm; sin ORM, en memoria. - Caché: si existe
jg g redis, los casos de uso aplican cache-aside (users:all,users:<id>, TTL 60 s) e invalidan en cada escritura. Sin Redis se generan sin caché. - Endpoints:
POST /,GET /,GET /:id,PATCH /:id(parcial),DELETE /:id. Validación → 400, no existe → 404, email repetido → 409.
Después de generarlo aplica el modelo (npm run prisma:migrate -- --name users, o drizzle:generate + drizzle:migrate).
jg g datasource user --orm <orm>
Intercambia la base de datos del CRUD en un comando. El dominio solo conoce el contrato UserDatasource, así que el cambio queda en infraestructura:
jg g datasource user --orm drizzle # prisma → drizzle
jg g datasource user --orm mongoose # drizzle → MongoDB
jg g datasource user --orm memory # para tests o demos
jg g datasource user --orm prisma # vuelta atrás instantánea- Crea
infrastructure/api/datasources/<orm>-user.datasource.tsy su mapper (y el modelo/tabla si faltan). Si ya existían, los reutiliza. - Reescribe solo
presentation/http/user/user.composition.ts, el composition root, que es el único archivo que nombra la clase concreta. - Entidad, DTOs, contratos, casos de uso, caché, controller y rutas no cambian.
- Los datasources anteriores se conservan;
jg undorestaura el composition root previo.
El ORM destino tiene que estar configurado (jg g <orm>) y con sus tablas creadas. Al pasar entre Postgres y Mongo cambian los ids (UUID ↔ ObjectId): si usas caché, vacíala. Si editaste el composition root a mano, el cambio lo regenera (lo anterior se recupera con jg undo).
jg g metrics
Deja la API monitoreada con Prometheus y Grafana sin escribir código: la app expone /metrics, Prometheus la lee cada 15 s y Grafana lo muestra en un dashboard ya armado. Requiere jg g express.
jg g metrics # /metrics abierta
jg g metrics --path /internal/metrics # otra ruta
jg g metrics --protect # exige Authorization: Bearer <token>
npm run dev
docker compose up -d prometheus grafana # Prometheus :9090 · Grafana :3001 (admin / admin)Qué se mide, sin instrumentar cada función:
- Peticiones HTTP (middleware): cantidad, duración y código de respuesta, etiquetadas por el patrón de la ruta (
/api/users/:id, nunca la URL real, para no crear una serie por id). - Casos de uso (Decorator
measured): duración y fallos, sin modificarlos. Si existe el CRUD de usuarios, sus cinco casos de uso se miden automáticamente yjg g datasourcelo conserva. Para otro caso de uso:measured(new MiCasoDeUso(...), getMetrics(), 'mi_caso')en su composition root. - Proceso: memoria, CPU, event loop.
Cómo queda:
- Dominio: contrato
MetricsServicey el Decoratormeasured. Infraestructura:PrometheusMetricsService(Singleton + Adapter sobre@prometheus-io/client) y su provider. Presentación: middleware, ruta y un router que se conecta enapp.tscomo primer middleware. Si cambiasteapp.tsy no encuentra dónde insertarlo, no lo toca y te dice qué línea añadir. monitoring/:prometheus.yml, datasource y dashboard de Grafana (provisionados, sin configurar nada a mano)..env:METRICS_PATH, puertos y usuario de Grafana; con--protect,METRICS_TOKEN(generado, solo en.env; en.env.examplequeda vacío).
503 inmediato cuando la app se satura (load shedding), activado por defecto (--no-load-shedding para omitirlo): si el retraso medio del event loop supera LOAD_SHEDDING_MAX_EVENT_LOOP_DELAY_MS (50 ms, la CPU no da abasto), responde 503 + Retry-After al instante a una parte de las peticiones, que sube de a poco hasta el 90% (nunca todas). LOAD_SHEDDING_MAX_IN_FLIGHT limita además las peticiones en curso; viene en 0 (sin límite) porque depende de cada app (capacidad × latencia aceptable): un valor fijo mal elegido rechaza de más cuando el límite es la base de datos. Así las aceptadas no se encolan hasta el timeout. /metrics y /api/health nunca se rechazan, y los rechazos se ven en el dashboard (ruta load_shedding). Ayuda cuanto más cara es cada petición (base de datos, CPU) frente a rechazarla, y cuando los clientes respetan Retry-After; con sobrecargas varias veces mayores que la capacidad hace falta escalar (más instancias o un proxy que limite delante).
Con --protect, como prometheus.yml no lee variables de entorno, la clave también se guarda en monitoring/prometheus/metrics.token (añadido al .gitignore) y se monta en el contenedor.
Acceder con la ruta protegida:
- Prometheus usa la clave solo; Grafana consulta a Prometheus, así que tampoco la necesita.
- A mano:
curl -H "Authorization: Bearer $(cat monitoring/prometheus/metrics.token)" localhost:3000/metrics. El navegador no envía cabeceras Bearer: para mirar, usa Grafana o Prometheus. - Al clonar el repo (la clave no se commitea) o para rotarla:
npm run metrics:tokengenera una nueva y la escribe en.envy enmetrics.token. Sin clave, la app no arranca ("Falta METRICS_TOKEN") en vez de quedar abierta. - En producción: define
METRICS_TOKENen el entorno del servidor y configura tu Prometheus con la misma clave. Prometheus lee la app enhost.docker.internal:<PORT>: si cambiasPORT, cámbialo también enprometheus.yml.
Deshacer cambios
Cada generación queda registrada en .jg/history.json (con su propio .gitignore, nunca se commitea).
jg history # 1 = la más reciente
jg undo # deshace la última
jg undo 3 # deshace las últimas 3
jg undo --dry-run # muestra qué revertiría
jg undo --force # revierte aunque hayas editado los archivosEl undo borra los archivos creados, restaura los modificados (docker-compose.yml, .env, schemas), elimina carpetas vacías, quita los scripts agregados y desinstala los paquetes que instaló. Si editaste un archivo después de generarlo, se detiene para no perder tu trabajo.
Configuración
jg init crea jg.config.json:
{
"srcDir": "src",
"importExtension": "",
"packageManager": null
}| Campo | Descripción |
|---|---|
| srcDir | Carpeta base del código |
| importExtension | "" para CommonJS/bundlers (Next.js, Vite); ".js" para ESM con "module": "NodeNext" |
| packageManager | Fuerza npm, pnpm, yarn o bun (por defecto se detecta) |
Agregar un generador
- Crea las plantillas en
templates/<nombre>/*.tpl. Variables disponibles:{{pascal}},{{camel}},{{kebab}},{{snake}},{{pluralCamel}},{{pluralSnake}},{{srcDir}},{{project}}. Para variantes usa bloques{{#if flag}}…{{else}}…{{/if}}con unvars: { flag: true }. - Crea
src/generators/<nombre>.jscon unplan()que declarefiles,dependencies,env,compose,scripts, etc. Los imports entre archivos se calculan solos conimports. - Regístralo en
src/generators/index.js.
Escribir archivos, editar el compose y el .env, instalar paquetes y el undo son comunes a todos los generadores. Usa src/generators/redis.js como referencia.
Desarrollo
git clone https://github.com/JavierGmnDEV/jagg-cli.git
cd jagg-cli
npm install
npm link # deja `jg` disponible globalmente apuntando a este códigoLicencia
MIT
