Saltearse al contenido

Registro de Cambios

Todos los cambios notables de Veloce-TS están documentados en este archivo.

El formato está basado en Keep a Changelog, y este proyecto sigue el Versionado Semántico.

Corregido — mezclar especificadores de import rompía los decoradores en silencio

Sección titulada «Corregido — mezclar especificadores de import rompía los decoradores en silencio»

veloce-ts y veloce-ts/plugins eran bundles separados, así que importar de ambos metía dos copias del framework en un mismo proceso. Las claves de metadata eran Symbol() a secas, único por instancia de módulo — así que una clase decorada a través de un especificador era invisible para el código cargado por el otro, sin error en ninguna parte.

Cómo se veía: el template fullstack importaba GraphQLPlugin de veloce-ts/plugins mientras su resolver usaba decoradores de veloce-ts. El plugin no encontraba metadata de resolvers, generaba un esquema vacío, y la primera query moría en buildSchema('') con “Syntax Error: Unexpected <EOF>” — un error que apunta a la query, que estaba bien. Rutas y guards registrados cruzando esa frontera habrían fallado igual.

  • La build ESM ahora usa code splitting, así que todos los subpaths comparten una copia del framework. La identidad de clases vuelve a funcionar — instanceof entre especificadores tenía el mismo problema. Deduplicar entre los 15 entrypoints además bajó el paquete de 0.45 MB a 0.39 MB.
  • Cada clave de metadata y token de DI es Symbol.for('veloce-ts:…'), resuelto por el registro global. CommonJS no se puede dividir, así que esto es lo que lo arregla para consumidores con require().

Un esquema vacío ahora se explica solo en vez de fallar dentro de graphql-js, y menciona esto como causa probable.

  • veloce new --install ejecuta el gestor de paquetes (Bun si está, npm si no); --git inicializa un repositorio con el primer commit, y se salta el paso si el destino ya está dentro de uno.
  • veloce generate gateway <nombre> genera un gateway @WebSocket y veloce generate listener <nombre> una clase de listeners @On. Ambas superficies existían sin scaffolding detrás.
  • Los proyectos generados traen .env.example con las variables que el código lee de verdad. Los templates configuran CORS desde CORS_ORIGINS desde la 3.1.0, documentado en ningún sitio donde alguien fuera a mirar.

Cambiado — ahora los templates enseñan el framework

Sección titulada «Cambiado — ahora los templates enseñan el framework»
  • Las dependencias se inyectan, no se construyen. El controlador y el resolver de ejemplo piden un UserService con @Inject(UserService); cambiar el array en memoria por una base de datos no toca a ningún llamador.
  • app.listen() se espera con await. Es async desde que existe la rama de Node, y sin el await un fallo al enlazar el puerto se volvía una promesa rechazada sin capturar en vez del error que el template imprime.
  • Los templates de GraphQL y fullstack activan las suscripciones y demuestran una de extremo a extremo con PubSub.
  • Los templates de GraphQL ahora dependen de graphql. Es un peer opcional, así que nada se ejecutaba sin él — y su ausencia no es un error de tipos, solo un 501 en la primera query.
  • El README coincide con lo que se escribió: estructura por template, endpoints reales, tabla de scripts, y el hecho de que el proyecto corre tanto en Node como en Bun. Antes mostraba un árbol genérico y apuntaba a /docs.html, que no es una ruta que se sirva.
  • veloce build --sourcemap viene desactivado por defecto.
  • Un proyecto GraphQL ya no trae un src/controllers/ vacío.

Cambiado — el smoke test de templates era demasiado débil para cazar nada de esto

Sección titulada «Cambiado — el smoke test de templates era demasiado débil para cazar nada de esto»

Solo comprobaba que una ruta respondiera con un estado no-5xx, y el endpoint de GraphQL devuelve 200 con un payload de errors cuando falta el paquete graphql — así que un template publicado sin esa dependencia parecía sano. Ahora ejecuta una mutación y una query reales, corre la comprobación exactamente una vez (reintentarla aplicaba la mutación dos veces), y se niega a arrancar si el puerto 3000 ya está ocupado.

Añadido — las suscripciones GraphQL se ejecutan de verdad

Sección titulada «Añadido — las suscripciones GraphQL se ejecutan de verdad»

Las suscripciones se generaban en el SDL y nada más: no había transporte de ejecución, así que subscribe no hacía nada y esta documentación lo decía. Era el hueco más grande que le quedaba al framework.

GraphQLPlugin las sirve ahora sobre graphql-transport-ws — el subprotocolo que hablan Apollo Client, urql, GraphiQL y el cliente graphql-ws — en Bun, Deno y Node.

const pubsub = new PubSub();
@Resolver()
class ChatResolver {
@GQLSubscription()
@Returns(MessageSchema, { name: 'ChatMessage' })
messageAdded() {
return pubsub.subscribe('MESSAGE_ADDED');
}
@GQLMutation()
async send(@Arg('input', SendMessage) input: SendMessage) {
await pubsub.publish('MESSAGE_ADDED', input);
return input;
}
}
app.usePlugin(new GraphQLPlugin({
resolvers: [ChatResolver],
subscriptions: {
// Un WebSocket de navegador no puede enviar cabecera Authorization, así que
// las credenciales llegan en connectionParams.
onConnect: ({ connectionParams }) => verify(connectionParams?.token),
},
}));
  • Desactivado por defecto. Abrir un endpoint WebSocket debería ser una decisión deliberada; subscriptions: true toma los valores por defecto.
  • Comparte la ruta de GraphQL, que es lo que asumen los clientes — un upgrade y un POST pueden convivir ahí porque son métodos distintos.
  • PubSub es la fuente: publish(topic, payload) / subscribe(topic), devolviendo el AsyncIterableIterator que quiere el subscribe() de graphql. Está construido sobre EventBus, así que pasar globalEvents hace que un listener @On('user.created') y una suscripción GraphQL reaccionen al mismo publish. Un resolver también puede devolver un async generator normal.
  • La contrapresión está acotada. maxQueueSize (1000) con drop-oldest, drop-newest o error decide qué cede cuando un suscriptor no da abasto.
  • No se filtra nada. Terminar una operación, cerrar el socket y app.shutdown() ejecutan todos el teardown del iterador, que desengancha su listener del bus.
  • Necesita el paquete graphql, ahora declarado como peer dependency opcional. En Node hace falta además @hono/node-ws, y la app debe servirse con app.listen().

La implementación del protocolo es propia del framework — sin dependencia en runtime de graphql-ws — así que la interoperabilidad se verifica en vez de darse por hecha: CI corre el servidor contra el cliente oficial graphql-ws bajo Node. Los códigos de cierre siguen la especificación: 4400 petición inválida, 4401 no autorizado, 4403 prohibido, 4408 timeout de init, 4409 suscriptor duplicado, 4429 demasiados init.

Ver GraphQL → Suscripciones.

  • app.listen(0) informaba puerto 0. La instancia devuelta llevaba el puerto solicitado, así que quien le pedía uno libre al sistema no tenía forma de saber cuál le tocó — el spread que debía recogerlo copia propiedades propias enumerables, y port vive en el prototipo del servidor tanto en Bun como en Node. Corregido en los tres runtimes.
  • Dos plugins WebSocket en una misma app Node se destruían las conexiones. WebSocketPlugin y el endpoint de suscripciones creaban cada uno su instancia de @hono/node-ws, y cada instancia engancha un listener de 'upgrade' que termina cualquier socket para el que no encuentra dueño. Ahora comparten un adaptador por aplicación.
  • Un conjunto de resolvers sin queries producía un esquema inservible. GraphQL exige una raíz de query en todo esquema, así que solo con suscripciones o mutaciones la primera operación fallaba con Query root type must be provided. Se emite una Query de relleno cuando nada más declara una.
  • El validate() de graphql lanza en vez de devolver errores cuando el esquema en sí es inválido. Dentro del manejador de mensajes de un socket eso tumbaba el proceso; ahora llega al cliente como mensaje error.
  • Los resolvers GraphQL no se podían resolver sobre un WebSocket. Se resolvían siempre en scope request contra un context.request que no existe en una conexión que sobrevive a cualquier petición. HTTP conserva el scope de petición; un socket resuelve como singleton.
  • Los resolvers de suscripción se enganchan al esquema ejecutable, así que la introspección describe ahora un tipo Subscription cuyos campos sí pueden correr. Antes se dejaban fuera a propósito.
  • Un resolver de suscripción que devuelve algo que no es un async iterable falla con un mensaje que nombra la clase y el método, en vez del genérico “Subscription field must return Async Iterable” de graphql-js.

Listeners de eventos declarativos, lo último que esta documentación listaba como ausente del bus de eventos. Una clase con métodos @On se registra con app.include() igual que un controlador, y su instancia sale del contenedor de DI — así que un listener inyecta los mismos servicios que un controlador.

class NotificationListeners {
constructor(@Inject(Mailer) private mailer: Mailer) {}
@On('user.created')
async welcome(payload: { email: string }) {
await this.mailer.send(payload.email, '¡Bienvenido!');
}
@On('app.ready', { once: true })
warmCaches() { /* … */ }
}
app.include(NotificationListeners);
  • Las suscripciones se enganchan en compile() y se sueltan en shutdown(). Sin eso, los listeners atados al globalEvents del proceso sobreviven a la app que los creó, y cada app construida en una suite de tests responde al mismo evento.
  • Una clase puede ser controlador y listener a la vez — include() ya no se detiene en la primera coincidencia.
  • new VeloceTS({ eventBus }) acepta un bus propio; por defecto usa globalEvents. app.getEventBus() devuelve el que está en uso.
  • Un @On que apunta a algo que no es un método falla en compile(), con la clase y la propiedad en el mensaje, en vez de en silencio al emitir.

Ver Event Bus.

Cambiado — metodología de benchmarks, y una corrección

Sección titulada «Cambiado — metodología de benchmarks, y una corrección»

La afirmación publicada de que Veloce-TS superaba a Hono crudo era falsa. La página de benchmarks y la documentación decían que Veloce-TS era más rápido que Hono crudo parseando cuerpos JSON (+16.7%) y validando con Zod (+43.8%). Esas cifras venían de una sola pasada cronometrada por framework, y una sola pasada no es una medición: tres corridas consecutivas del escenario idéntico pusieron a Hono adelante por 2%, luego 17%, luego empatado — y una cuarta puso a Veloce adelante por 14%.

Con rondas repetidas y mediana, Hono lidera en todos los escenarios, que es la única respuesta sensata para una capa construida sobre Hono. Coste medido de esa capa: 2–11%. Contra Express 4 es ~1.8× más rápido (el README decía 10×) y ~1.3–1.4× más rápido que Fastify 5.

El harness ahora corre 7 rondas, reporta una mediana con su rango intercuartílico, calibra el número de iteraciones contra tiempo de reloj, y marca los resultados demasiado ruidosos para compararse. La suite HTTP rota el orden de los servidores entre rondas para que el calentamiento no favorezca al que corrió primero.

  • Los fallbacks de versión decían 0.3.0. veloce --version y, peor, el rango de dependencia que veloce new escribe en el package.json generado caían a una versión fija que llevaba mucho sin ser la actual. veloce new además consultaba primero el package.json del directorio actual — el proyecto en el que estuvieras parado, que no es veloce-ts.
  • veloce new podía colgarse en la consulta a npm. La petición al registry no tenía timeout; ahora se rinde a los 5 segundos y genera con la versión instalada, avisándolo.
  • Un archivo inexistente se servía como 200 con cuerpo vacío bajo Bun. Bun.file() es perezoso y nunca lanza para una ruta que no existe, así que Response.file() lo pasaba directo a la respuesta. La rama de Node, que hace stat primero, devolvía 404 correctamente — la misma llamada respondía distinto según el runtime.

La cobertura sobre src/ pasó de 50.17% a ~57%, concentrada en código que casi no tenía y que carga riesgo real: los stores respaldados por Redis (caché, sesión, blacklist de tokens, rate limit, antes ~4%), WebSocketConnection, la contención de raíz de Response.file(), y request-context. El gate de cobertura en CI sube de 50% a 55% — estaba puesto exactamente en el valor medido, lo que lo hacía un cable trampa en vez de un trinquete.

Añadido — los WebSockets funcionan en Node

Sección titulada «Añadido — los WebSockets funcionan en Node»

WebSocketPlugin lanzaba al arrancar en Node (“requires Bun or Deno runtime”), lo que dejaba inutilizables ahí los decoradores @WebSocket / @OnConnect / @OnMessage — y los templates websocket y fullstack. Era la limitación número uno documentada del framework.

Node ahora hace el upgrade a través de @hono/node-ws, una peer dependency opcional. Bun y Deno quedan intactos: siguen haciendo el upgrade nativamente y no necesitan paquete extra.

Ventana de terminal
npm install @hono/node-ws # solo hace falta en Node

Toda la superficie del gateway se verifica de extremo a extremo en Node en CI: handshake del upgrade, @OnConnect, @OnMessage, salas, broadcast() y @OnDisconnect. El smoke test de templates ahora arranca los cuatro en vez de saltarse los dos que no podían correr.

authorizeUpgrade() sigue ejecutándose, con una diferencia que conviene saber: en Bun y Deno un cliente no autorizado se rechaza durante el handshake con 401, mientras que @hono/node-ws es dueño de la ruta y entrega el socket ya abierto — así que en Node la conexión se acepta y luego se cierra con 1008 Policy Violation.

El plugin engancha el manejador WebSocket en onStart(), porque necesita el http.Server real, que solo existe después de app.listen(). Sirve con app.listen(); si le pasas app.fetch a un servidor que creaste tú, el plugin se queda sin nada a lo que engancharse.

  • WebSocketConnection leía WebSocket.OPEN del global, que solo se estabilizó en Node 22 — en Node 20, que engines sigue soportando, tocarlo lanzaba un ReferenceError. Ahora usa la constante numérica que fija el estándar.
  • Los parámetros de socket se tipan estructuralmente (WebSocketLike) en vez de contra el WebSocket del DOM, así que un socket de Bun, uno de Deno y el WSContext de node-ws encajan todos.
  • VeloceTS.getServer() expone el servidor en ejecución, y ServerInstance.raw lleva el objeto propio del runtime.

Las builds de producción ya no emiten sourcemaps. Bun incrusta el texto completo de cada archivo fuente en el mapa vía sourcesContent, y con una build ESM y otra CJS eso significaba publicar todo el código TypeScript dos veces: los sourcemaps eran el 73% del tarball. Cada instalación lo pagaba para servir el caso raro de entrar a las tripas del framework con un depurador — clona el repo para eso.

EmpaquetadoDescomprimido
3.1.11.47 MB6.9 MB
3.1.20.41 MB1.73 MB

Las builds de desarrollo conservan sourcemaps inline, que es donde se usan de verdad.

Los mapas de declaración tampoco se emiten ya. Los 256 archivos .d.ts.map resolvían a ../../src/*.ts, y files no publica src/ — así que eran 351 KB de mapas apuntando a nada en un paquete instalado.

Corregido — los tipos publicados eran inservibles en node16/nodenext

Sección titulada «Corregido — los tipos publicados eran inservibles en node16/nodenext»
  • Ningún especificador relativo de las declaraciones llevaba extensión, así que cualquier consumidor con moduleResolution: node16 o nodenext solo obtenía TS2834 (“Relative import paths need explicit file extensions”) desde node_modules/veloce-ts, y perdía todos los tipos. Los 332 especificadores relativos de src/ llevan ahora un .js explícito.
  • TypeScript rechazaba require('veloce-ts') con TS1479 aunque funciona en runtime. Un solo árbol de declaraciones servía a ambas condiciones, y TypeScript infiere el tipo de módulo de un .d.ts desde el package.json más cercano — la raíz dice "type": "module", así que los tipos declaraban ESM mientras require resolvía a dist/cjs. La build ahora emite un árbol de declaraciones CommonJS aparte y cada condición de exports lleva sus propios types.
  • WebSocketManager.handleMessageBun tipaba un parámetro como Buffer, lo que obligaba a los consumidores a instalar @types/node solo para typechequear un import del paquete. Ahora es Uint8Array.

Paridad con Node en la CLI. Todo lo que genera veloce new ahora se crea, compila, construye y ejecuta con Node puro, no solo con Bun.

Corregido — la CLI generaba proyectos que no arrancaban

Sección titulada «Corregido — la CLI generaba proyectos que no arrancaban»
  • veloce new generaba una app que reventaba al arrancar. Los templates rest y fullstack emitían cors: { origin: '*', credentials: true }, la combinación que la 3.0.0 empezó a rechazar al construir, así que un proyecto recién creado moría antes de servir una sola petición.
  • veloce build fallaba en Node con ReferenceError: Bun is not defined: llamaba a Bun.build sin condiciones. Ahora compila con el tsc del propio proyecto cuando no corre bajo Bun.
  • veloce dev solo lanzaba bun. Ahora detecta el runtime y recurre a tsx, que transpila los decoradores Stage-2 que el type stripping propio de Node no maneja.
  • Node no podía cargar la salida compilada. Los templates importaban módulos relativos sin extensión, que tsc emite tal cual y el cargador ESM de Node rechaza: node dist/index.js moría con ERR_MODULE_NOT_FOUND. Los templates y la salida de veloce generate ahora escriben la extensión .js, que Bun resuelve de vuelta al fuente .ts.
  • Los templates websocket y fullstack pasaban una opción handlers que no existe en WebSocketPluginConfig, así que esos proyectos no pasaban el type-check. Los gateways se registran con app.include(), igual que los controladores.
  • A los proyectos generados les faltaba @hono/node-server, así que app.listen() lanzaba en Node.
  • --runtime <auto|bun|node> en veloce dev y veloce build. auto prefiere Bun cuando es utilizable y si no toma el camino de Node.
  • Los proyectos generados son agnósticos del runtime: los scripts pasan por el binario veloce en vez de fijar bun, engines.node es >=20, tsx viene como devDependency para el servidor de desarrollo en Node, y se incluye un script typecheck.
  • veloce new avisa al generar un template con WebSocket si Bun no está instalado, en vez de dejarte una app que lanza al primer arranque.
  • Smoke test de templates en CI (npm run test:templates): empaqueta el árbol de trabajo, genera los cuatro templates contra ese tarball y luego instala, compila, construye y arranca cada uno con Node. Es justo la comprobación que faltaba cuando se publicaron los templates rotos.

Bun.build es una API en proceso, así que el camino de build con Bun exige que la propia CLI corra bajo Bun. npm run build en una máquina con Bun instalado igual toma el camino de Node, porque npm arranca la CLI con Node.

  • MemoryRateLimitStore y RedisRateLimitStore no se exportaban. La 3.0.0 introdujo el store de rate limit intercambiable y lo documentó, pero ninguna de las dos clases se reexportaba desde veloce-ts ni desde veloce-ts/middleware, así que un consumidor no podía pasar RateLimitOptions.store. Ahora ambas se exportan desde los dos puntos de entrada; los tipos RateLimitStore y RateLimitHit ya eran accesibles vía veloce-ts/types.

Revisión completa del código. Actualiza si usas @Auth(), @Session(), @RequireCSRF() o la familia @CanAccess — esos guards nunca se aplicaron en 2.x, así que las rutas que debían proteger respondían a peticiones anónimas. Consulta MIGRATION.md.

  • Los guards de ruta nunca se aplicaban. AuthPlugin, SessionPlugin y PermissionPlugin reemplazaban app.compile desde dentro de su propio install(), pero install() se ejecuta dentro del compile() que ya está corriendo, así que el reemplazo nunca se ejecutaba. En la práctica una ruta con @Auth() sin parámetro @CurrentUser() respondía 200 a peticiones anónimas, @Auth({ roles: ['admin'] }) admitía a cualquier usuario autenticado, y @Session({ required: true }) / @RequireCSRF() no hacían nada. Los tres plugins ahora inyectan su guard en el middleware de la ruta durante install(), igual que ya hacía RBACPlugin. @Roles() no se vio afectado: RBAC era el único plugin bien conectado.
  • Las cookies de sesión ahora se firman con SessionConfig.secret (HMAC-SHA256, comparación en tiempo constante). Antes la cookie llevaba el id de sesión en crudo, así que cualquiera podía sustituirlo por otro y recibir esa sesión. secret ahora es obligatorio, y las cookies usan secure: true por defecto con NODE_ENV=production.
  • La rotación de refresh tokens es atómica. refreshAccessToken() verificaba primero y ponía en lista negra después, así que dos peticiones concurrentes con el mismo token tenían éxito las dos y cada una recibía un par válido. Ahora el token viejo se reclama antes de emitir el par nuevo; la perdedora falla.
  • El cuerpo de las peticiones tiene límite de tamaño. El nuevo VeloceTSConfig.bodyLimit (1 MiB por defecto, 0 lo desactiva) rechaza cuerpos grandes con 413 antes de que ningún handler los lea.
  • Las respuestas cacheadas pueden variar por llamante. @Cache() ahora respeta varyByHeaders y el nuevo keyGenerator(c); antes la clave era solo método + ruta + params, así que una ruta cacheada por usuario podía servir el cuerpo de un usuario a otro. Cachear una ruta que lee @CurrentUser() con una clave independiente del llamante ahora emite un aviso al compilar.
  • Los flujos de auth ya no reportan fallos internos como 401. Una caída de base de datos dentro de login/register/refresh se envolvía en AuthenticationException(error.message), reportando un fallo de infraestructura como credenciales inválidas y filtrando el mensaje interno.
  • Los errores de handlers WebSocket ya no se devuelven al cliente. Solo se responden problemas de parseo o esquema; el resto se registra en el servidor.
  • Las entradas de revocación JWT se indexan por jti en vez del token completo, así que la lista negra ya no guarda credenciales reutilizables.
  • CORS: se emite Vary: Origin cuando el origen permitido depende de la petición, y origin: '*' junto a credentials: true ahora lanza al configurar, en vez de producir respuestas que el navegador rechaza en silencio.
  • Response.file() gana la opción root (y Response.fileFrom(root, path)) que confina la búsqueda a un directorio; las rutas que se salen reciben 403.
  • SessionConfig.secret es obligatorio. El formato de la cookie cambia, así que las sesiones emitidas por 2.x se rechazan y esos usuarios vuelven a iniciar sesión.
  • bodyLimit vale 1 MiB por defecto. Súbelo si aceptas subidas de archivos: new Veloce({ bodyLimit: 25 * 1024 * 1024 }).
  • docs vale true por defecto y ahora sí monta OpenAPI en /openapi.json y /docs. Registrar OpenAPIPlugin a mano sigue teniendo prioridad; usa docs: false para desactivarlo.
  • engines.node sube a >= 20 (Node 18 llegó a fin de vida).
  • Adapter.listen() puede devolver Promise<ServerInstance> — solo afecta a adaptadores propios.
  • TokenBlacklist recibe ids de token (jti) en vez de tokens crudos; los stores propios pueden implementar el claim() opcional para rotación atómica.
  • El constructor de respuestas se exporta como VeloceResponse (y Res). El export Response anterior ensombrecía el Response global del Web API en cualquier archivo que lo importara; sigue disponible como alias obsoleto.
  • @UseInterceptor a nivel de clase nunca se ejecutaba — la búsqueda leía target.constructor sobre un valor que ya era la clase.
  • Los filtros de excepción nunca veían errores de validación ni de inyección de dependencias. Ahora también les llegan los errores lanzados por middleware y guards de ruta.
  • DIContainer reportaba dependencias circulares falsas con concurrencia. Los caminos de resolución ahora son por contexto asíncrono, y las resoluciones concurrentes de un singleton comparten una sola promesa, así que se crea exactamente una instancia.
  • Dos controladores con el mismo nombre de clase se pisaban las rutas. El registro usaba target.name; ahora usa la identidad de la clase.
  • Las opciones docs y plugins se aceptaban y se ignoraban.
  • Un cuerpo JSON malformado devolvía 422 “Expected object, received null” — ahora es un 400 que nombra el problema de JSON.
  • A AuthService.refresh() le faltaba un await, así que su catch nunca corría y quien lo llamaba recibía un Error pelado en vez de InvalidTokenException.
  • El listen() de Node nunca pudo funcionar. Usaba require('@hono/node-server'), y el bundle ESM publicado no tiene require en scope, así que siempre lanzaba y reportaba el paquete como ausente. Ahora usa import dinámico.
  • Los generadores de SSE/streaming que lanzan ahora marcan el stream con error y lo registran, en vez de producir un rechazo no capturado.
  • Los listeners process.on('SIGTERM'/'SIGINT') que registraban el rate limiter y MemorySessionStore anulaban el manejo de señales por defecto de Node para toda la aplicación.
  • RateLimitStore con MemoryRateLimitStore (por defecto) y RedisRateLimitStore, para aplicar un límite compartido entre instancias en vez de por proceso.
  • Variables de contexto tipadas — c.get('auth.user') viene tipado y una clave mal escrita es un error de compilación.
  • VeloceTSConfig.bodyLimit y @Cache({ keyGenerator }).

Revisión profunda de seguridad, empaquetado y rendimiento. Si despliegas en Node.js, actualiza — la 1.2.0 publicada estaba rota ahí. Consulta MIGRATION.md para los dos cambios incompatibles.

  • Un refresh token podía reutilizarse como access token. JWTProvider.verifyAccessToken() ignoraba el claim type del payload, y como refreshSecret cae por defecto a secret cuando no se define, un refresh token — con vida por defecto de 7 días — verificaba correctamente como access token. Ahora se rechaza cualquier payload con type === 'refresh'.
  • RS256/RS384/RS512 nunca funcionaron de verdad. Se aceptaban como JWTConfig.algorithm, pero la firma siempre usaba el secret compartido, así que los tokens iban firmados con HMAC y la garantía asimétrica no existía. Se añadieron privateKey/publicKey; el constructor ahora lanza si se configura un algoritmo RS* sin ambas.
  • El rate limiting confiaba en cabeceras falsificables. El keyGenerator por defecto leía X-Forwarded-For/X-Real-IP sin condiciones, así que cualquier cliente podía falsificar su identidad de rate limit. Ahora se ignoran salvo que se active la nueva opción trustProxy: true; el modo por defecto usa la IP real del peer.
  • Los fallos de @ResponseSchema se tragaban, dejando que salida malformada del handler llegara al cliente. Ahora se registran y devuelven 500.
  • Los métodos de verificación y revocación de JWTProvider ahora son async: verifyAccessToken, verifyRefreshToken, refreshAccessToken, blacklistToken, isBlacklisted, cleanupBlacklist. Esto habilita almacenes de revocación intercambiables. Olvidar el await es peligroso, no solo incorrecto — la Promise devuelta siempre es truthy.
  • Los controladores con decoradores ahora se resuelven como singleton, no una instancia nueva por petición. El estado por petición en campos de instancia ahora se comparte. Puedes optar por lo anterior con @Controller(prefix, { scope: 'transient' }).
  • API funcional tipada — app.get/post/put/delete/patch y app.route() son genéricos sobre el conjunto de schemas Zod declarado, así que los argumentos del handler se infieren de extremo a extremo sin casts. El orden de inyección es body, query, params, headers; solo los schemas declarados aportan un argumento.
  • Se eliminó any de la superficie pública: configuración de plugins, configuración del store de caché, metadatos de parámetros, el registro de metadatos de auth/RBAC/sesión, y los tipos de retorno del adaptador Express.
  • @Controller(prefix, { scope }) — scope de instanciación explícito (singleton | request | transient).
  • RedisTokenBlacklist / MemoryTokenBlacklist y la interfaz TokenBlacklist — revocación de tokens que se propaga entre réplicas. La lista negra en memoria nunca lo hizo, así que el logout fallaba en silencio al revocar en otras instancias.
  • Plugin.onStart(app) / Plugin.onStop(app) — hooks de ciclo de vida. El contrato era solo de instalación, así que un plugin con recursos abiertos no tenía forma de liberarlos. onStop corre en orden inverso de instalación recolectando errores.
  • app.getFetchHandler() — la forma de export fetch(request, env, ctx) que esperan los runtimes serverless/edge. Es la vía soportada para desplegar en Cloudflare Workers, donde listen() no puede funcionar.
  • CacheManager.destroy() — libera los timers y conexiones de todos los stores.
  • Endurecimiento de WebSocket — heartbeatIntervalMs, heartbeatTimeoutMs, idleTimeoutMs, maxMessageSizeBytes. Los mensajes demasiado grandes reciben un frame de error y código de cierre 1009.
  • @Returns() para tipos de retorno en GraphQL, y createTimeoutMiddleware() exportado desde veloce-ts/middleware.
  • LoggerConfig.pretty ahora se respeta vía un transporte pino-pretty — antes se aceptaba y se ignoraba.
  • CI ahora corre una matriz multi-runtime real: Bun fijado + latest en ubuntu/windows/macos, un job de smoke en Node 20/22 que importa los artefactos ESM y CJS construidos, una etapa Deno tolerante a fallos, validación de paquete aislada, y un gate de cobertura que sube su reporte.
  • El CLI corre bajo Node puro. La entrada bin apuntaba a un archivo TypeScript con #!/usr/bin/env bun, así que no podía ejecutarse sin Bun pese a que el paquete anuncia Node >= 18.
  • El fallo al generar .d.ts ahora es fatal en builds de producción; antes avisaba y continuaba, así que un release podía publicar tipos rotos con el pipeline en verde.
  • La validación de paquete verifica que cada condición de exports resuelva a un archivo real y tipa un consumidor generado contra dist/types. Esto es lo que detectó las rutas rotas de adapters/*.
  • veloce generate openapi ahora usa el OpenAPIGenerator real en lugar de un constructor artesanal que ignoraba Zod, y el cliente generado emite interfaces reales derivadas de los schemas en lugar de Promise<any>.

La 1.2.0 publicada no funcionaba para consumidores de Node:

  • veloce-ts/adapters/* no resolvía — el mapa exports apuntaba a dist/{esm,cjs}/adapters/*.js pero el build genera dist/{esm,cjs}/src/adapters/*.js.
  • Las dependencias de runtime ya no se empaquetan dentro de dist. Empaquetarlas enviaba una segunda copia de cada una, lo que rompía la identidad de Zod (el framework comprueba schemas creados con tu zod; una copia empaquetada es una clase distinta) y re-transpilaba código de terceros, dejando el regex de semver inválido y lanzando al hacer require. El paquete ahora pesa 1.33 MB, contra 3.82 MB antes.
  • El bundle ESM emitía import.meta.require, un global exclusivo de Bun; se cambió al target node.
  • import { sign } from 'jsonwebtoken' rompía el cargador ESM de Node — es un paquete solo-CJS. Ahora se importa vía su export por defecto.
  • dist/cjs/*.js se interpretaba como ESM porque el package.json raíz declara "type": "module". El build ahora genera dist/cjs/package.json con {"type":"commonjs"}.
  • GraphQL nunca ejecutaba. Los resolvers se pasaban como una forma anidada de rootValue que graphql-js no lee, así que toda query resolvía a null, y el tipo de retorno de cada campo estaba fijado a String (los tipos de objeto nunca se emitían). Los resolvers ahora se adjuntan directamente a los campos del schema, y los tipos de objeto/entrada se derivan de Zod. Las suscripciones siguen sin transporte de ejecución.
  • El dispatch de peticiones es 1.9–2.6× más rápido. La capa de “metadata compilada” precomputaba un regex de ruta, ordenamientos de índices y flags de tipo que el handler nunca leía; los controladores se re-instanciaban por petición; la búsqueda de interceptores hacía dos lecturas de reflect por petición; y los módulos de caché/contexto/auth se cargaban con await import() dentro del hot path. Todo se resuelve ahora una sola vez en compile().
  • Doble rollback en BaseTransactionManager — un handler que terminaba bien pero estaba marcado como rollbackOnly lanzaba, el mismo catch hacía rollback otra vez, y el “transaction not found” resultante enmascaraba el error real.
  • app.shutdown() nunca ejecutaba los handlers de onShutdown() — solo lo hacía el camino de SIGTERM/SIGINT.
  • CacheManager.reset() filtraba el intervalo de limpieza del store anterior, acumulando timers y manteniendo procesos vivos.
  • El “LRU” de la caché en memoria era en realidad FIFO — desalojaba por tiempo de inserción, no por último acceso. Ahora get() actualiza la recencia.
  • Los stores Redis de caché y sesión usaban el comando bloqueante KEYS en clear()/deletePattern()/length()/all(), que congela el event loop de Redis con keyspaces grandes. Ahora usan SCAN por cursor con DEL por lotes.
  • EventBus: un listener que lanzaba abortaba el resto (Promise.all). Ahora todos se ejecutan; los fallos se recolectan y se relanzan juntos como AggregateError.
  • Response.file() lanzaba en runtimes distintos de Bun — ahora hace streaming vía node:fs.
  • El adaptador Express bufferizaba el cuerpo completo de la respuesta, rompiendo @SSE() y @Stream() (los clientes no recibían nada hasta que el generador terminaba). Ahora hace streaming por chunks con manejo de contrapresión.
  • BaseRepository.count()/updateMany()/deleteMany() hacían escaneos completos de tabla en silencio (count() cargaba en memoria cada fila coincidente; las escrituras masivas hacían fetch-y-bucle). Ahora lanzan NotImplementedError; los tres adaptadores incluyen sobrescrituras nativas.
  • Dos manejadores de SIGTERM/SIGINT en competencia con timeouts de apagado separados se consolidaron en uno.
  • Los parámetros de query se validaban dos veces por petición con formas de error inconsistentes.

Re-medidos y republicados; los números anteriores estaban etiquetados como v0.4.3 y se habían quedado obsoletos a lo largo de varios releases. El dispatch de peticiones mejoró entre 87% y 158% respecto a v0.4.18. Frente a otros frameworks, Veloce-TS supera a Hono crudo en parseo de body JSON (+16.7%) y validación Zod (+43.8%), y es ~33% más lento en una ruta trivial /hello. Los tests pasaron de 509 a 781.

  • veloce generate controller <nombre> — genera clase @Controller con stubs de rutas CRUD
  • veloce generate service <nombre> — genera una clase de servicio plana
  • veloce generate module <nombre> — genera controlador + servicio + DTO + barrel en src/modules/<nombre>/
  • veloce generate resolver <nombre> — genera clase @Resolver con stubs de @GQLQuery y @GQLMutation
  • veloce generate dto <nombre> — genera esquema Zod + tipo TypeScript inferido
  • veloce generate middleware <nombre> — genera skeleton de middleware Hono
  • veloce generate plugin <nombre> — genera scaffold de clase VelocePlugin
  • app.onShutdown(handler) — registra un handler async; se ejecutan en orden inverso al de registro al recibir SIGTERM/SIGINT
  • app.setShutdownTimeout(ms) — configura el tiempo máximo de espera antes de process.exit(1) (por defecto: 30 000 ms)
  • app.registerShutdownSignals() — llamado automáticamente por app.compile(); adjunta process.on('SIGTERM') y process.on('SIGINT')
  • Interfaz ExceptionFilter<T> — implementa catch(error: T, c: Context): Response para producir respuestas de error tipadas
  • @Catch(...errorClasses) — vincula una clase de filtro a uno o más tipos de error
  • app.useFilter(filter) — registra un filtro global; se comprueba en orden de registro, gana el primero que coincide
  • FilterManager — gestor interno; exportado para integraciones personalizadas
  • Interfaz Interceptor — implementa intercept(ctx: ExecutionContext, next: () => Promise<Response>): Promise<Response>
  • ExecutionContext — contexto tipado con campos request, controller y handler
  • @UseInterceptor(...interceptors) — aplica interceptores a un método o clase; se apilan alrededor del handler como middleware
  • app.useInterceptor(interceptor) — registra un interceptor global; se ejecuta antes de los de clase/método
  • @SSE() — marca un handler AsyncGenerator como flujo de Server-Sent Events (text/event-stream)
  • @Stream(contentType) — marca un handler AsyncGenerator como flujo raw con tipo de contenido personalizado (p. ej. 'text/csv')
  • EventBus — pub/sub en proceso con on, once, off, emit (async, paralelo), emitSync, removeAllListeners, listenerCount
  • globalEvents — singleton compartido EventBus para comunicación entre módulos sin inyección de dependencias
  • @Throttle(limit, windowMs) — límite de tasa por endpoint que anula la configuración global
  • @ApiVersion(v) — antepone /v<v> a la ruta del endpoint para APIs versionadas
  • @ResponseHeader(name, value) — establece una cabecera de respuesta fija para la ruta
  • @Redirect(url, status?) — devuelve una respuesta de redirección (estado por defecto: 302)
  • @Deprecated(message?) — marca la ruta como obsoleta en OpenAPI y registra un aviso al arrancar
  • isolate() — restablece todo el estado singleton (CacheManager, registros de plugins) entre ejecuciones de test en paralelo
  • compileTestApp(app) — compila la app y devuelve la instancia Hono; wrapper de conveniencia para tests unitarios
  • Versión de la especificación actualizada de 3.0.x a 3.1.0
  • Añadido jsonSchemaDialect: 'https://spec.openapis.org/oas/3.1/dialect/base'
  • nullable: true normalizado a { type: ['x', 'null'] } o { oneOf: [T, { type: 'null' }] } mediante normalizeFor31()

Ninguno — todos los cambios son aditivos y retrocompatibles con v1.0.1.


  • Pipeline de decoradores GraphQL: @Resolver/@GQLQuery/@GQLMutation ahora funcionan de extremo a extremo. GraphQLSchemaBuilder leía de los Maps de la instancia de MetadataRegistry que nunca se llenaban (los decoradores escribían con un namespace de símbolos local separado). Ahora lee directamente de reflect-metadata vía getResolverMetadata/getFieldsMetadata.
  • PermissionManager.revokePermission(): El filtro resourceId ? p.resourceId !== resourceId : true siempre devolvía true cuando no se pasaba resourceId, por lo que nunca se eliminaba nada. Corregido.
  • Opción resolvers en GraphQLPlugin: Pasa las clases resolver directamente — new GraphQLPlugin({ resolvers: [UserResolver] }) — como alternativa a app.include(). Ambas rutas se combinan y deduplicarán automáticamente.
  • Reconocimiento de resolvers en app.include(): include() detecta ahora las clases decoradas con @Resolver y registra sus metadatos, de modo que app.include(UserResolver) alimenta al constructor del esquema GraphQL automáticamente.
  • 89 tests nuevos (509 en total): PermissionPlugin (16), HealthCheckPlugin (25), Logger (20), OAuthPlugin (28), decoradores/pipeline GraphQL (8 nuevos, 17 en total en la suite).

  • CORS en respuestas de error: las respuestas del manejador de errores (401, 422, 500, application/problem+json, etc.) incluyen Access-Control-Allow-Origin y el resto de cabeceras CORS configuradas, alineadas con las respuestas OK (mergeVeloceCorsHeaders, VELOCE_CORS_HEADERS_KEY, VeloceCorsHeadersSnapshot).
  • Mismo parche CORS que en 0.4.4; npm no permite republicar una versión tras unpublish, así que 0.4.5 es la versión instalable.

No usar: el tarball en npm llegó roto (404). Tras unpublish, el registro bloquea reutilizar ese número. Usa 0.4.5 para el arreglo de CORS.


  • RFC 9457 (Problem Details): errores por defecto con Content-Type: application/problem+json, campos type, title, status, detail, instance y extensiones (violations / details en validación; debug en 500 solo en desarrollo).
  • VeloceTSConfig.errorResponseFormat: 'rfc9457' (defecto) o 'legacy' para el JSON { error, statusCode, details? } anterior.
  • Helpers exportados: problemTypeUri, resolveProblemType, resolveProblemTitle, buildProblemInstance, toLegacyErrorBody, sendErrorResponse, PROBLEM_JSON_MEDIA_TYPE, DEFAULT_PROBLEM_TYPE_BASE.
  • HTTPExceptionOptions: problemType y title opcionales en HTTPException.
  • Excepciones de auth usan el formato unificado con tipos de problema authentication-error / authorization-error.
  • Cabeceras @module en núcleo, errores, validación, DI, respuestas, logging, middleware, adaptadores, testing y barrels.
  • Benchmarks actualizados para v0.4.3.
  • El cuerpo de error sigue incluyendo error y statusCode junto a los campos RFC 9457. Para application/json en errores, usa errorResponseFormat: 'legacy'.

  • HealthCheckPlugin: los nombres de los checkers se asignan de forma segura en Bun (Object.defineProperty + respaldo) para que no falle al poner .name en funciones async.
  • Export @Req: el decorador de parámetro ya se exporta en la entrada principal del paquete (import { Req } from 'veloce-ts').
  • Guías y referencia API actualizadas para @Ctx(), @Req() y la API real de HealthCheckPlugin (array checks, HealthCheckers.database / memory / disk).

  • include() ya no descarta los campos definidos por decoradores (como statusCode de @HttpCode y responseSchema de @ResponseSchema) al registrar rutas de controladores.
  • Suite de tests completa (53 tests) que cubre: API funcional, rutas con decoradores, validación de body/query, excepciones HTTP y contenedor DI.
  • Logger de respaldo basado en console: si pino no está instalado, el framework ya no falla al arrancar — usa console automáticamente.
  • pino, pino-pretty e ioredis movidos a optionalDependencies — ya no se instalan automáticamente (~7 MB menos por defecto). Instálalos explícitamente con bun add pino pino-pretty o bun add ioredis si los necesitas.
  • winston eliminado de las dependencias (estaba listado pero nunca fue usado por el framework).
  • Bundle principal reducido de 444 KB → 408 KB (ESM minificado).
  • Export FastAPITS: usa VeloceTS o el alias corto Veloce en su lugar. FastAPITS será eliminado en v1.0.0.

Esta versión representa la mayor actualización desde el lanzamiento inicial. Se añadieron más de 25 mejoras distribuidas en tres oleadas de trabajo, cubriendo la cadena completa desde la generación de rutas hasta la documentación OpenAPI, el testing, el ORM y la CLI.

Cinco decoradores de una sola línea como alternativa concisa a @ApiDoc({...}):

  • @Summary(text) — descripción corta visible en la lista de Swagger UI
  • @Description(text) — texto largo en el panel de detalle de la operación
  • @Tag(name) — asigna un tag individual; apilable con múltiples @Tag
  • @Tags(...names) — asigna varios tags en un solo decorador
  • @Deprecated() — marca la ruta como obsoleta (tachado en Swagger UI)
  • @HttpCode(statusCode) — sobreescribe el código HTTP de respuesta del handler
  • @ResponseSchema(schema, statusCode?) — valida y sanitiza la respuesta con un esquema Zod; informa el modelo de respuesta al spec de OpenAPI
  • @Timeout(ms, message?) — aborta la petición con 408 Request Timeout si el handler supera el límite. Emite el header X-Timeout-Ms
  • @RateLimit(options) — aplica rate-limiting a nivel de ruta individual. Los headers estándar X-RateLimit-* se envían automáticamente
  • Auto-tagging: deriva tags automáticamente del primer segmento del path sin necesidad de anotarlos manualmente
  • Bearer security scheme: añade components.securitySchemes.bearerAuth al spec y aplica security: [{ bearerAuth: [] }] en rutas protegidas automáticamente
  • 401 automático: rutas protegidas reciben una respuesta 401 Unauthorized documentada sin configuración adicional
  • Soporte de @HttpCode y @ResponseSchema en el spec generado

Seis nuevas clases de excepción:

  • ConflictException (409), GoneException (410), PayloadTooLargeException (413)
  • UnprocessableEntityException (422), TooManyRequestsException (429), ServiceUnavailableException (503)
  • Errores 5xx se registran con getLogger().error incluyendo path, método, status y stack
  • Errores 4xx se registran como warn en entorno de desarrollo

🧪 TestClient — API fluida y helpers de autenticación

Sección titulada «🧪 TestClient — API fluida y helpers de autenticación»

Reescritura completa de TestClient:

  • TestResponse con métodos encadenables: expectStatus(), expectOk(), expectCreated(), expectJson(), expectField(), expectHeader(), expectArrayLength()
  • withToken(token), withHeaders(headers) — instancias inmutables con auth pre-configurada
  • loginAs(credentials, endpoint?) — hace login y retorna cliente autenticado
  • registerAndLogin(user, endpoints?) — registra y hace login en una sola llamada
  • clearAuth() — limpia el token almacenado
  • registerDrizzle(app, db, token?) — registra como singleton en el DIContainer
  • @InjectDB(token?) — decorador de parámetro, alias de @Depends(DB_TOKEN)
  • DB_TOKEN — símbolo por defecto para el token de inyección
  • PaginationMeta incluye from y to (rango 1-based)
  • createCursorPaginatedResult con nuevo parámetro hadPrevCursor
  • createMultiCursor y decodeMultiCursor para cursores compuestos
  • paginate<T>(data, total, page, limit) — helper standalone
  • parseCursorQuery() y PaginationHelper.parsePaginationQuery() mejorados

Reescritura completa:

  • Carga Express de forma lazy usando Function('return require')()
  • Acepta instancia de Express pre-creada como segundo argumento
  • Manejo correcto de body raw vs. parseado
  • Delega errores al pipeline de Express via next(err)
  • ZodError cross-module: añadido fallback error.name === 'ZodError' para garantizar 422 en todos los casos
  • Rutas GET marcadas públicas retornaban 401: corregido con app.on(['POST', 'PUT', 'DELETE'], ...)
  • Cache collision en MetadataCompiler: solucionado con IDs únicos por función vía WeakMap
  • @Cache / @UseMiddleware perdían metadatos: solucionado actualizando el registro en cada decorador

La respuesta 422 ahora incluye: field (formato items[0].price), received, expected, minimum/maximum

Ninguno — todos los cambios son retrocompatibles.


  • Serialización de Respuestas JSON: Corregido bug crítico de serialización
  • Resolución de Versión CLI: CLI ahora obtiene correctamente la última versión de npm
  • Compilación de la Aplicación: Corregida la falta de await app.compile() en plantillas generadas
  • Obtención de versión corregida desde el registro npm
  • Todas las plantillas incluyen await app.compile()

Lanzamiento de parches con correcciones de build internas. Sin cambios funcionales.


  • Obtención de Última Versión: CLI obtiene automáticamente la última versión desde npm
  • Mejor Manejo de Errores: Mensajes más claros y limpieza al fallar la creación de proyectos
  • Indicadores de Progreso: Indicadores visuales mejorados durante la creación
  • Plantillas: Todas incluyen la llamada obligatoria a await app.compile()

  • Caché en Memoria: Caché LRU rápida con limpieza automática
  • Caché Redis: Soporte de caché distribuida para despliegues multi-instancia
  • Decorador @Cache(): Caché de respuestas declarativa con configuración flexible de TTL
  • Decorador @CacheInvalidate(): Invalidación de caché basada en patrones
  • Headers X-Cache: Headers automáticos HIT/MISS en respuestas
  • IDs de Petición Automáticos: Generación de UUID para cada petición
  • Decorador @RequestId(): Inyecta el ID de petición en métodos del controlador
  • Decorador @AbortSignal(): Soporte de cancelación para operaciones largas
  • Header X-Request-ID en todas las respuestas
  • Integración automática del ID de petición en todos los logs
  • Loggers hijo con herencia contextual
  • Logs JSON estructurados para producción
  • Pretty printing para desarrollo
  • createRequestContextMiddleware(), createSimpleRequestIdMiddleware()
  • createCacheMiddleware(), createCacheInvalidationMiddleware()

Ninguno — todos los cambios son aditivos y retrocompatibles.


  • Exportación de Query: Añadida la exportación faltante de Query del índice principal
  • Decoradores de Parámetro: @Query de HTTP ahora se exporta correctamente junto a los decoradores de GraphQL
  • Conflicto de Query en GraphQL: Eliminado el alias conflictivo Query de los decoradores GraphQL
  • Los decoradores GraphQL ahora usan GQLQuery para evitar conflictos
  • Las queries de GraphQL ahora usan @GQLQuery en lugar de @Query
  • Decorador @Query: Ahora maneja parámetros sin esquemas correctamente
  • Soporta @Query(), @Query('param') y @Query(Schema)
  • Exportaciones WebSocket: Corregida la exportación faltante del decorador @WebSocket
  • Decoradores GraphQL: Añadidos los alias faltantes Query, Mutation y Subscription
  • Sistema de Autenticación Completo: JWT con tokens de acceso/refresco
  • Control de Acceso Basado en Roles (RBAC): Roles jerárquicos con sistema de permisos
  • Soporte WebSocket en Tiempo Real: Decoradores para WebSocket
  • Integración GraphQL: Soporte completo con resolvers y suscripciones
  • Herramienta CLI: Scaffolding de proyectos y generación de código
  • Lanzamiento inicial del framework veloce-ts
  • Routing basado en decoradores
  • Validación automática con Zod
  • Sistema de inyección de dependencias
  • Generación automática de documentación OpenAPI
  • Soporte para WebSocket y GraphQL
  • CLI para scaffolding de proyectos
  • Soporte multi-runtime (Bun, Node.js, Deno, Cloudflare Workers)

:::tip Última Versión Recomendamos usar siempre la última versión de Veloce-TS para obtener la mejor experiencia y las últimas funciones. :::

:::note Breaking Changes Los cambios que rompen compatibilidad están claramente marcados en cada lanzamiento. Revisa el registro antes de actualizar a una nueva versión mayor. :::