Category: JavaScript

  • Nuxt 4: la estructura app/ que rompe los tutoriales viejos

    Nuxt 4: la estructura app/ que rompe los tutoriales viejos

    Nuxt 3 recibe parches —bug fixes y seguridad— hasta el 31 de julio de 2026. A partir de esa fecha, ninguno. Lo dice la roadmap oficial de Nuxt.

    Si lees esto en julio de 2026, te quedan días. Si lo lees después, la fecha ya pasó y tu proyecto en Nuxt 3 corre sin soporte oficial.

    Y aquí viene lo divertido: si hoy buscas "tutorial Nuxt 4" vas a encontrar decenas de artículos escritos en julio de 2025, la semana del lanzamiento. Te van a decir que crees una carpeta pages/ en la raíz del proyecto. Vas a hacerlo, no va a funcionar, y vas a perder cuarenta minutos pensando que te has equivocado en la instalación.

    No te has equivocado. El tutorial está desfasado. Nuxt 4 movió esa carpeta de sitio y ese es exactamente el cambio del que casi nadie ha actualizado su contenido.

    Este post está escrito contra Nuxt 4.5.0, la versión estable en julio de 2026. No contra la 4.0 del día del lanzamiento, que es lo que describe casi todo el material que vas a encontrar.

    ¿Qué es Nuxt 4?

    Nuxt 4 es el meta-framework de Vue: coge Vue, que por sí solo es una librería de UI, y le añade routing por sistema de ficheros, renderizado en servidor, un servidor HTTP propio (Nitro), generación estática, auto-imports y un sistema de módulos. La versión 4 se publicó el 16 de julio de 2025 e introdujo el cambio más visible respecto a Nuxt 3: el código de aplicación se movió a la carpeta app/.

    Si vienes de Angular o de React, la analogía más limpia es esta: Nuxt es a Vue lo que Next.js es a React, o lo que Analog aspira a ser en Angular. Un meta-framework — todo lo que necesitas para llevar una aplicación a producción, ya montado.

    La tesis de Nuxt es que tú no deberías estar configurando nada de eso. Creas un fichero en el sitio correcto y el framework infiere la intención. Esto encanta o irrita, según de dónde vengas. Si llegas de Angular, donde todo es explícito y declarado, la cantidad de magia implícita te va a chirriar los primeros días. Es una decisión de diseño consciente, no un descuido.

    Estructura de directorios en Nuxt 4: la carpeta app/

    En Nuxt 4 el código de aplicación vive dentro de app/. Las carpetas pages/, components/, composables/, layouts/, middleware/, plugins/ y utils/, junto a app.vue, app.config.ts y error.vue, van en app/ — no en la raíz del proyecto como en Nuxt 3. Las carpetas de servidor y recursos (server/, public/, content/, layers/, modules/ y shared/) se quedan en la raíz. Lo tienes en la documentación oficial.

    Si solo te llevas una cosa del post, que sea esta.

    En Nuxt 3 todo eso vivía junto en la raíz: pages/, components/ y composables/ mezclados con server/, public/ y los ficheros de configuración. La raíz ahora queda para lo que no es aplicación Vue.

    Así queda un proyecto:

    mi-proyecto/
    ├── app/
    │   ├── assets/
    │   ├── components/
    │   ├── composables/
    │   ├── layouts/
    │   ├── middleware/
    │   ├── pages/
    │   ├── plugins/
    │   ├── utils/
    │   ├── app.vue
    │   ├── app.config.ts
    │   └── error.vue
    ├── server/
    ├── public/
    ├── content/
    ├── layers/
    ├── modules/
    ├── shared/
    ├── nuxt.config.ts
    ├── package.json
    └── tsconfig.json
    

    Fíjate bien en la segunda mitad. server/, public/, content/, layers/, modules/ y shared/ no están dentro de app/. Se quedan en la raíz. Igual que node_modules/, .nuxt/ y .output/.

    El fallo número uno al seguir material antiguo es crear pages/ en la raíz. En Nuxt 4 va en app/pages/. El framework no la encuentra donde tú la has puesto y no pasa nada visible: simplemente no tienes rutas y no entiendes por qué.

    ¿Por qué el cambio? Porque tener todo el código en la raíz obligaba a los file watchers a escanear .git/ y node_modules/, algo que retrasa el arranque de forma notable en sistemas que no son macOS. Y la separación entre lo que corre en el navegador y lo que corre en el servidor por fin es visible a simple vista, además de darte mejores autocompletados en el IDE.

    Una nota sobre app/pages/: es opcional. Puedes construir una aplicación de una sola pantalla solo con app/app.vue y no crear la carpeta nunca. Pero en el momento en que quieras rutas y mantengas tu propio app.vue, necesitas poner <NuxtPage> dentro para que Nuxt tenga dónde renderizar la página actual. Si no lo pones, el enrutador funciona y no ves nada en pantalla.

    Crear el proyecto y arrancarlo

    Requisito previo: Node.js 22.x o superior. La documentación recomienda usar la LTS activa. Si tienes una 20 por ahí de otro proyecto, cámbiala antes de empezar o vas a pelearte con errores que no dicen lo que pasa.

    Para crear el proyecto:

    npm create nuxt@latest mi-proyecto
    

    Con pnpm:

    pnpm create nuxt@latest mi-proyecto
    

    Con bun:

    bun create nuxt@latest mi-proyecto
    

    Y para arrancar el servidor de desarrollo abriendo el navegador automáticamente:

    npm run dev -- -o
    

    Con pnpm es pnpm dev -o y con bun bun run dev -o. Ojo al doble guion en la versión de npm: sin él, npm se come el flag y no lo pasa a Nuxt.

    Lo que vas a usar todos los días

    Rutas por ficheros

    Cada fichero .vue dentro de app/pages/ genera una URL:

    app/pages/
      ├── index.vue       → /
      ├── about.vue       → /about
      └── posts/
          └── [id].vue    → /posts/:id
    

    Los corchetes marcan segmentos dinámicos. Dentro del componente, lees el parámetro con useRoute():

    <script setup lang="ts">
    const route = useRoute()
    console.log(route.params.id)
    </script>
    

    Para navegar entre páginas sin recargar, <NuxtLink>:

    <template>
      <NuxtLink to="/about">Sobre mí</NuxtLink>
      <NuxtLink to="/posts/1">Primer post</NuxtLink>
    </template>
    

    Nada de esto se importa. Los auto-imports de Nuxt te dan useRoute, NuxtLink y todo lo que haya en app/components/ sin una sola línea de import. Al principio desorienta. A la semana no quieres volver atrás.

    Data fetching: $fetch vs useFetch vs useAsyncData

    Aquí hay tres herramientas y elegir mal es el error más común de quien llega nuevo.

    Herramienta Cuándo usarla Segura en SSR Deduplica
    $fetch Acciones del usuario: click, envío de formulario No No
    useFetch Datos iniciales de un componente
    useAsyncData Lógica asíncrona que no es una simple llamada HTTP Sí, por clave

    $fetch es la utilidad básica de red. No tiene protección para SSR ni deduplicación de peticiones. Úsala para interacciones del cliente disparadas por un evento: un click, el envío de un formulario.

    useFetch es el envoltorio seguro para SSR. Hace la petición una sola vez en renderizado universal, en lugar de dispararla en servidor y otra vez en cliente al hidratar. Es lo que quieres para los datos iniciales de un componente:

    <script setup lang="ts">
    const { data, status, error, refresh } = await useFetch('/api/posts')
    </script>
    

    useAsyncData hace lo mismo pero con control más fino, y recibe una clave única como primer argumento para la caché. Es tu opción cuando la lógica asíncrona no es una simple llamada HTTP.

    Ambos devuelven data, error, status, y funciones refresh, execute y clear. Y aceptan opciones que conviene conocer desde el día uno: lazy para no bloquear la navegación, server: false para pedir solo en cliente, pick para recortar el payload que viaja al navegador y watch para refrescar cuando cambie un valor reactivo.

    Rutas de servidor

    Esto es lo que a mí me terminó de convencer de Nuxt: el backend vive en el mismo proyecto y no es un añadido de segunda.

    La carpeta server/ en la raíz se escanea sola:

    server/
    ├── api/          # rutas prefijadas con /api
    ├── routes/       # rutas sin prefijo
    ├── middleware/   # corre antes de cada handler
    ├── plugins/      # hooks del ciclo de vida de Nitro
    └── utils/        # helpers propios
    

    Un fichero server/api/hello.ts responde en /api/hello. Un fichero en server/routes/ responde sin el prefijo. Todos los handlers usan defineEventHandler:

    export default defineEventHandler((event) => {
      return { hello: 'world' }
    })
    

    Para leer datos de la petición tienes tres utilidades que también son globales:

    // server/api/hello/[name].ts
    export default defineEventHandler((event) => {
      const name = getRouterParam(event, 'name')
      return `Hola, ${name}`
    })
    
    // server/api/submit.post.ts
    export default defineEventHandler(async (event) => {
      const body = await readBody(event)
      return { body }
    })
    

    getQuery(event) te da los parámetros de query. Y el sufijo del nombre del fichero define el método HTTP: test.get.ts atiende los GET, test.post.ts los POST, y cualquier otro método devuelve un 405.

    Un aviso importante sobre esto. readBody te devuelve lo que venga, sin garantías de forma ni de tipo. En una ruta de servidor pública eso es una puerta abierta. Aquí es donde un schema de validación deja de ser buena práctica y pasa a ser obligatorio:

    import { z } from 'zod'
    
    const schema = z.object({
      email: z.string().email(),
      plan: z.enum(['free', 'pro']),
    })
    
    export default defineEventHandler(async (event) => {
      const result = schema.safeParse(await readBody(event))
    
      if (!result.success) {
        throw createError({ statusCode: 400, statusMessage: 'Payload inválido' })
      }
    
      // result.data viene tipado, no hace falta castear nada
      return { ok: true, email: result.data.email }
    })
    

    Validas el body antes de tocarlo y de paso obtienes el tipo inferido gratis. Si no tienes ese reflejo instalado, el curso de Zod cubre exactamente este patrón y se traslada tal cual a Nuxt.

    Novedades de Nuxt 4.5

    La 4.0 fue una versión de estabilidad. Nuxt 4.5 es donde está lo interesante.

    Vite 8. Este es el titular. Arranques en frío más rápidos e internals movidos a Rolldown. Si tienes plugins de Vite propios, revisa la guía de migración antes de actualizar, porque algo se te va a mover. Sobre lo que trae Vite 8 en sí escribí un análisis aparte, y aplica entero aquí: cuando actualizas Nuxt, estás actualizando también tu bundler.

    Rspack 2 sobre Rsbuild. El builder alternativo se moderniza. La configuración no cambia:

    export default defineNuxtConfig({
      builder: 'rspack',
    })
    

    SSR streaming, experimental. Envía el HTML por partes en lugar de esperar a tenerlo todo. Mejora bastante el Time to First Byte:

    export default defineNuxtConfig({
      experimental: {
        ssrStreaming: true,
      },
    })
    

    Códigos de error estables. Los errores y avisos ahora vienen con un identificador fijo tipo NUXT_E1001, con explicación en línea y enlace a la documentación. Suena menor. No lo es: convierte "esto peta y no sé por qué" en una búsqueda con un solo resultado correcto.

    useLayout. Un composable para leer el layout resuelto de la ruta actual:

    const layout = useLayout()
    

    Named views por convención de nombre. Múltiples salidas de renderizado usando el nombre del fichero:

    app/pages/parent/child.vue
    app/pages/parent/child@sidebar.vue
    

    La opción enabled en useFetch y useAsyncData. Condiciona si la petición puede ejecutarse:

    const query = ref('')
    
    const { data } = await useFetch('/api/search', {
      query: { q: query },
      enabled: () => query.value.length > 2,
    })
    

    Sustituye la guarda manual dentro del watch por una barrera declarativa. Pero léete la letra pequeña antes de usarlo: mientras enabled es false se bloquea todo, incluidos execute, refresh y los disparos del watch. Y volver a true no relanza la petición por sí solo. Necesitas una fuente reactiva — la opción query del ejemplo — para que se dispare. Si pones solo enabled con una URL estática, la petición nunca sale y no hay ningún error que te lo diga.

    Cuándo Nuxt tiene sentido y cuándo no

    Voy a ser honesto, porque un post que solo vende no te sirve de nada.

    Nuxt encaja cuando necesitas SEO real con contenido dinámico, cuando quieres frontend y backend en un mismo repositorio sin montar dos despliegues, y cuando el equipo ya conoce Vue. También encaja en productos que empiezan pequeños y aún no sabes si necesitarán servidor: Nitro te deja cambiar de destino de despliegue casi sin tocar código.

    Nuxt no encaja si lo que tienes es una landing estática con tres secciones. Ahí estás pagando un runtime que no necesitas y Astro hace ese trabajo con menos JavaScript enviado al navegador. Tampoco encaja si tu equipo es de Angular y no hay ninguna intención de mantener Vue: el meta-framework no es el problema, el ecosistema paralelo sí.

    Y hay un tercer caso que veo mucho: aplicaciones detrás de login, sin SEO, donde una SPA normal resuelve igual. Puedes usar Nuxt en modo cliente, claro. Pero si desactivas lo que lo hace especial, pregúntate qué estás comprando.

    Sobre Nuxt 5: está en desarrollo e incluirá Nitro v3 más cambios adicionales. La roadmap oficial marcaba Q1 de 2026 como estimación, una fecha que ya quedó atrás. Lo relevante para ti no es cuándo sale, sino la política: Nuxt se compromete a soportar cada major un mínimo de seis meses tras la salida del siguiente. Traducido: cuando aparezca la 5, tu proyecto en 4 tiene medio año de margen garantizado. Esa previsibilidad vale más que cualquier feature.

    Qué hacer hoy

    Si mantienes un proyecto en Nuxt 3, el soporte oficial termina el 31 de julio de 2026. Pasada esa fecha no hay parches de seguridad. Empieza por lo aburrido: sube a Node 22, actualiza la dependencia y mueve tu código de aplicación dentro de app/. Ese movimiento de carpetas es el grueso de la migración, y no hace falta que lo hagas a mano — hay un codemod oficial:

    npx codemod@latest nuxt/4/file-structure
    

    Si no has tocado Nuxt nunca, crea un proyecto con el comando de arriba, mete un server/api/hello.ts, consúmelo con useFetch desde una página y observa la petición en la pestaña de red. Vas a ver que en la primera carga no hay ninguna llamada al API: el servidor ya trajo los datos. Ese momento explica Nuxt mejor que cualquier artículo.

    Y si quieres construir algo real con esto sin pasar tres semanas dando vueltas a la arquitectura, es exactamente el proceso que trabajo en el curso de Construye con IA: de idea a producto con especificación primero y sin caos. En Dominicode Labs seguimos estos cambios de versión según salen, con los proyectos completos.

    Preguntas frecuentes

    ¿Puedo seguir usando Nuxt 3 después del 31 de julio de 2026?

    Tu aplicación va a seguir funcionando, no se apaga sola. Lo que termina es el soporte: Nuxt 3 recibe actualizaciones de mantenimiento con corrección de bugs y parches de seguridad hasta finales de julio de 2026. A partir de ahí, un fallo de seguridad en el framework se queda sin arreglar oficialmente. Para cualquier cosa en producción, esa es razón suficiente para planificar la migración.

    ¿Es obligatorio mover mi código a la carpeta app/?

    No. La documentación oficial dice literalmente que la migración "no es obligatoria": Nuxt autodetecta la estructura antigua y sigue funcionando. Dicho eso, app/ es la convención oficial y lo que asumen el material y los módulos nuevos, así que retrasarlo solo aplaza el trabajo. El código de aplicación (assets, components, composables, layouts, middleware, pages, plugins, utils, más app.vue, app.config.ts y error.vue) va dentro de app/, mientras que server/, public/, content/, layers/, modules/ y shared/ se quedan en la raíz.

    ¿Cómo migro de Nuxt 3 a Nuxt 4?

    Actualiza a Node.js 22, sube la dependencia de Nuxt y mueve el código de aplicación dentro de app/. Ese movimiento de carpetas es el grueso del trabajo y hay un codemod oficial que lo automatiza: npx codemod@latest nuxt/4/file-structure. La configuración (nuxt.config.ts) y la carpeta server/ se quedan donde están.

    ¿Puedo mantener la estructura de Nuxt 3 en Nuxt 4?

    Sí. Nuxt 4 autodetecta la estructura antigua y funciona sin cambios. También puedes forzarla explícitamente con srcDir: '.' en nuxt.config.ts. Es una salida válida para ganar tiempo en un proyecto grande, no una decisión que quieras mantener a largo plazo.

    ¿Cuál es la diferencia entre useFetch y $fetch?

    $fetch es la utilidad de red básica, sin protección para SSR ni deduplicación, pensada para peticiones disparadas por eventos del usuario. useFetch la envuelve y garantiza que en renderizado universal los datos se piden una sola vez, sin repetir la llamada al hidratar en el cliente. Regla simple: datos iniciales del componente con useFetch, acciones del usuario con $fetch.

    ¿Qué versión de Node.js necesito para Nuxt 4?

    Node.js 22.x o superior, y la documentación recomienda usar la versión LTS activa. Si vienes de un entorno con Node 20, actualiza antes de crear el proyecto: los errores por versión antigua no siempre indican con claridad cuál es la causa real.

    ¿Necesito crear la carpeta app/pages/ siempre?

    No, es opcional. Una aplicación de una sola pantalla puede vivir solo en app/app.vue. Ahora bien, si quieres routing por ficheros y mantienes tu propio app.vue, tienes que incluir el componente <NuxtPage> dentro para que Nuxt sepa dónde renderizar la página activa.

    ¿Cómo escribo un endpoint de API en Nuxt 4?

    Creas un fichero dentro de server/api/ en la raíz del proyecto y exportas un defineEventHandler. Un server/api/hello.ts queda disponible en /api/hello. Para leer datos usas getRouterParam(event, 'nombre'), getQuery(event) y await readBody(event). El método HTTP se define con el sufijo del fichero, como submit.post.ts.

    ¿Nuxt 4 usa TypeScript por defecto?

    Sí, los proyectos vienen configurados con TypeScript desde el inicio y Nuxt genera tipos automáticamente para rutas, componentes y las respuestas de tus endpoints de servidor. Si te interesa hacia dónde va el tipado en general, escribí sobre el nuevo compilador de TypeScript en Go y cómo cambia los tiempos de compilación.


    Si prefieres ver esto en vídeo, en el canal de YouTube de Dominicode publico este tipo de análisis de versiones cada semana.

    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • ExpressoTS 4.0, el framework TypeScript que planta cara a NestJS

    ExpressoTS 4.0, el framework TypeScript que planta cara a NestJS

    Cada cierto tiempo alguien me escribe con la misma pregunta: "Bezael, tengo que montar una API en Node. ¿Express pelado o NestJS?".

    Y la respuesta honesta durante años ha sido incómoda. Express te deja solo: sin DI, sin estructura, sin ciclo de vida. NestJS te da todo eso, pero a cambio de decoradores por todas partes y una curva que a un junior le cuesta semanas.

    En medio no había casi nada. Ese hueco es exactamente donde vive ExpressoTS 4.0, publicada el 17 de julio de 2026, casi veinte meses después de la v3, y que por primera vez no parece un proyecto experimental.

    Ojo con lo que voy a decir y con lo que no voy a decir. No voy a contarte que esto reemplaza a NestJS. No lo hace. El ecosistema de NestJS es incomparablemente más maduro: integraciones, documentación, gente que ya resolvió tu problema hace dos años. Lo que sí ha cambiado es que ahora existe una alternativa seria para quien quiere estructura sin cargar con todo el peso.

    Qué es ExpressoTS y por qué la 4.0 importa

    ExpressoTS es un framework TypeScript para backend sobre Node.js. Trae inyección de dependencias con contenedor IoC, routing, middleware, hooks de ciclo de vida, manejo de errores y bootstrap de la aplicación.

    Hasta la v3 era, básicamente, "Express con DI y decoradores". Útil, correcto, poco ambicioso.

    La 4.0 es otra cosa. Requiere Node.js >= 20.19.0, según las release notes oficiales de la v4.0.0, y trae bloques que hasta ahora te tocaba construir tú o importar de tres librerías distintas.

    Dónde queda cada uno, para que no tengas que deducirlo:

    Express ExpressoTS 4.0 NestJS
    Inyección de dependencias No Sí, contenedor IoC Sí, contenedor IoC
    Curva de aprendizaje Baja Media Alta
    Boilerplate Mínimo Moderado Alto
    Testing incluido No Sí, createTestApp()
    Observabilidad incluida No Sí, Studio local Vía integraciones
    Madurez del ecosistema Muy alta Baja Muy alta

    Voy a ir a los bloques que de verdad cambian cómo escribes el código.

    Interceptors: AOP sin montarte tu propio framework

    Los interceptors de ExpressoTS 4.0 aplican cross-cutting concerns —caching, reintentos, transformación de respuestas— con ejecución condicional declarativa: la condición vive en el decorador, no dentro del interceptor.

    Este es el titular de la release, y no había visto la idea tan limpia en otros frameworks del ecosistema.

    // PerformanceInterceptor viene incluido. CacheInterceptor lo escribes tú.
    @UseInterceptors(
        PerformanceInterceptor,
        whenInterceptor(
            (ctx) => ctx.request.headers["x-cache"] === "true",
            CacheInterceptor,
        ),
    )
    

    Léelo despacio. El PerformanceInterceptor corre siempre. El CacheInterceptor corre solo si la request trae esa cabecera. No hay un if dentro del interceptor decidiendo si le toca trabajar o no. La condición vive fuera, en la declaración.

    Parece un detalle estético. No lo es. La cantidad de código que he visto en producción donde un interceptor empieza con seis líneas de "¿me toca actuar ahora?" es descorazonadora. Ahí es donde nacen los bugs de "en staging cachea y en prod no".

    Tienes también unlessInterceptor() para el caso inverso, auto-discovery por decoradores y tres interceptors listos de fábrica: LoggingInterceptor, PerformanceInterceptor y TimeoutInterceptor.

    Y para componerlos, dos utilidades que redondean el argumento: pipeInterceptors() los encadena en orden, y combineInterceptors() los lanza en paralelo para trabajo de solo efecto secundario, como logging o métricas.

    Eventos tipados con prioridad

    El sistema de eventos de ExpressoTS 4.0 combina auto-discovery de handlers, routing condicional y ejecución por prioridad, con tipado end-to-end vía IEventHandler<T>:

    @OnEvent(UserCreatedEvent, { priority: 1 })
    export class SendWelcomeEmailHandler implements IEventHandler<UserCreatedEvent> {
        handle(event: UserCreatedEvent) { /* ... */ }
    }
    

    El IEventHandler<UserCreatedEvent> es lo que hace que esto valga la pena. En cuanto alguien cambie la forma del evento, el compilador te avisa en todos los handlers.

    Sin eso, un sistema de eventos es una lista de strings mágicos esperando a romperse en el peor momento.

    Configuración type-safe y validación pluggable

    La configuración se declara con validación y variación por entorno:

    export default defineConfig({
        database: {
            url: Env.string("DATABASE_URL", { required: true }),
        },
    });
    

    Si falta DATABASE_URL, la aplicación no arranca. No revienta a los veinte minutos en la primera query. Falla en el arranque, que es donde debe fallar.

    Y esto conecta con una decisión de diseño que aplaudo: la Smart Validation de v4 usa un registry de adapters pluggable. Soporta class-validator, Zod y Yup. No te casan con una librería.

    Si vas a montar algo nuevo con esto, mi recomendación es Zod. Esquema y tipo en la misma declaración, sin decoradores, sin duplicar la forma del dato en dos sitios. Y si te preocupa el coste de tipado en un proyecto grande, ese problema tiene fecha de caducidad: ya conté cómo el compilador de TypeScript reescrito en Go cambia el juego.

    Si nunca has llevado Zod más allá de z.object(), en el curso de Zod para TypeScript cubro justo la parte que la gente se salta: transforms, refinements y validación en los bordes del sistema.

    ExpressoTS Studio: local, no cloud

    ExpressoTS Studio es una plataforma de desarrollo local: no envía tu tráfico a ningún servidor externo. Es la decisión más valiente de la release y merece sección propia.

    Te da un dashboard de estado, un mapa de arquitectura generado en vivo desde el grafo de dependencias, un request timeline con spans de OpenTelemetry, logs en directo, inspección de errores, replay de tráfico y una auditoría de seguridad con scoring basada en el tráfico real de tu entorno de desarrollo.

    El mapa de arquitectura generado desde el grafo DI es la parte que más me gusta. Documentación de arquitectura que no se queda obsoleta porque nadie la actualiza: se deriva del código.

    Y que sea local en vez de SaaS elimina de golpe la conversación con legal antes de empezar.

    El resto, en corto

    Hay más, y no todo necesita párrafos:

    • Lazy-loading de módulos con rutas auto-detectadas desde @controller() y preload hints (high, medium, low, never). Debería mejorar los cold starts, que en serverless es dinero.
    • Módulo de testing con createTestApp() a cero configuración, API fluida para HTTP, snapshot testing y load testing con métricas de percentiles.
    • Logging de 11 fases: structured logging, transports a fichero, gestión de contexto, consulta de logs y export a Markdown.
    • Guards por rol, por permiso y resource-owner, con utilidades de composición.
    • Health monitoring en tres capas: middleware pipeline, providers IHealthCheck y dashboard agregado.
    • Content negotiation RFC 7231: JSON, XML, CSV y YAML.
    • Scopes DI personalizados: tenant, transaction, workflow, session. Si haces multi-tenant, esto te ahorra un patrón entero.
    • API versioning por URL con el decorador @Version().
    • Errores RFC 7807 (problem details) con exception filters y sugerencias de ruta en los 404.
    • Lifecycle hooks: globalConfiguration(), configureServices(), postServerInitialization(), serverShutdown().

    Si vienes de v3: lo que se rompe

    Migrar de v3 a v4 tiene tres breaking changes obligatorios:

    1. Los patrones de DI cambian. Revísalos uno a uno.
    2. Los lifecycle hooks de app.ts hay que actualizarlos.
    3. Sube el runtime a Node.js >= 20.19.0.

    En soporte, el equipo ha sido razonable: según su política publicada, v4.0.0 recibe 24 meses de bugfixes y parches de seguridad. La v3.x tenía 18 meses y los han extendido hasta diciembre de 2026 para que la migración no sea una carrera.

    Una migración así es un caso de manual para trabajar con especificación antes que con código: describes el estado destino, listas los puntos de cambio y solo entonces dejas que un agente te ayude a ejecutarlo módulo a módulo. Es la metodología que documenté en el libro de Spec-Driven Development, y funciona especialmente bien cuando el cambio es amplio pero mecánico.

    Mi veredicto

    ExpressoTS 4.0 no es un juguete. Interceptors condicionales, eventos tipados, scopes DI de tenant y transaction, Studio local: son decisiones de gente que ha sufrido aplicaciones grandes.

    ¿Lo llevaría a un proyecto crítico, con equipo de quince personas y entrega en tres meses? Todavía no. NestJS tiene ecosistema, integraciones probadas y una comunidad enorme, y eso pesa más que cualquier feature bonita cuando algo te falla un viernes.

    ¿Lo usaría en un servicio nuevo, un side project o una API interna donde el peso y los cold starts importan? Sin dudarlo.

    Instálalo y móntate algo pequeño esta semana:

    npx @expressots/cli new my-app
    

    Levanta Studio, mira el mapa de arquitectura que genera del grafo DI y decide con tu propio código delante. Media hora te basta para saber si te encaja. La documentación oficial está sorprendentemente bien para un proyecto de este tamaño.

    Y si quieres ver cómo integro frameworks nuevos como este en un flujo de trabajo con agentes de IA —specs primero, implementación asistida, tests de verdad— eso es justo lo que describo en mi stack de IA agéntica y lo que practicamos dentro de Dominicode Labs.

    Preguntas frecuentes

    ¿ExpressoTS 4.0 sustituye a NestJS?

    No, y no lo pretende. NestJS tiene un ecosistema mucho más maduro en integraciones, documentación y comunidad. ExpressoTS ocupa el hueco entre Express pelado y NestJS: te da DI, ciclo de vida y estructura con menos boilerplate y una curva más corta. Son opciones distintas, no una sustitución.

    ¿Qué diferencia hay entre ExpressoTS y Express?

    Express es un router HTTP minimalista: no trae inyección de dependencias, ni estructura de proyecto, ni ciclo de vida de aplicación. ExpressoTS se construye sobre esa base y añade contenedor IoC, decoradores para controllers, hooks de ciclo de vida, guards, interceptors y utilidades de testing. Con Express decides tú toda la arquitectura; con ExpressoTS parte ya viene decidida.

    ¿Qué versión de Node necesito para ExpressoTS 4.0?

    Node.js 20.19.0 o superior, según las release notes oficiales de la v4.0.0 (17 de julio de 2026). Es un breaking change respecto a v3, así que verifica el runtime de tu entorno de despliegue antes de migrar.

    ¿ExpressoTS Studio envía mis datos a la nube?

    No. Studio es una plataforma de desarrollo local. El dashboard, el mapa de arquitectura, el request timeline con spans OpenTelemetry, los logs y la auditoría de seguridad funcionan sobre el tráfico de tu entorno de desarrollo, en tu máquina.

    ¿Puedo usar Zod para validar en ExpressoTS 4.0?

    Sí. La Smart Validation de v4 funciona con un registry de adapters pluggable que soporta class-validator, Zod y Yup. Puedes elegir la librería que ya uses en el resto del proyecto.

    ¿Cuánto tiempo tengo para migrar desde v3?

    El soporte de v3.x se ha extendido hasta diciembre de 2026. La v4.0.0 recibe 24 meses de bugfixes y parches de seguridad desde su publicación en julio de 2026. Tienes margen para planificar la migración sin prisas.

    ¿Qué se rompe al migrar de ExpressoTS v3 a v4?

    Tres cosas: los patrones de inyección de dependencias cambian y hay que revisarlos uno a uno, los lifecycle hooks de app.ts necesitan actualizarse, y el runtime debe subir a Node.js 20.19.0 o superior.

    ¿ExpressoTS 4.0 sirve para serverless?

    Es uno de los escenarios donde mejor encaja. El lazy-loading de módulos carga solo lo necesario en cada invocación, con preload hints (high, medium, low, never) para afinar qué se precarga, lo que ayuda con los cold starts. Súmale que el core es ligero comparado con alternativas más pesadas del ecosistema.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Método HTTP QUERY: RFC 10008 explicado para developers

    Método HTTP QUERY: RFC 10008 explicado para developers

    Hace un par de años me tocó construir el buscador de un CRM interno. Filtros combinables por nombre, etiqueta, estado, rango de fechas, un par de campos personalizados que el cliente quería poder cruzar entre sí.

    Nada del otro mundo. O eso pensé.

    Me pasé una tarde entera peleando con un problema que HTTP, tal cual lo conocíamos hasta ahora, no resolvía bien. Spoiler: la solución llegó en 2026, se llama método HTTP QUERY, y llevaba más de veinte años de retraso.

    ¿Por qué una tarde entera por un simple buscador? Porque en cuanto diseñas el endpoint te topas con el mismo dilema de siempre.


    El dilema de siempre: GET o POST

    GET es la opción "correcta" semánticamente. No cambia nada en el servidor, se puede repetir sin miedo, y cualquier intermediario puede guardar la respuesta en caché.

    El problema es práctico: en cuanto combinas más de cuatro o cinco filtros —arrays, rangos, objetos anidados— serializarlos en un query string se vuelve una tortura. Y las URIs tienen límites de tamaño reales, que servidores, proxies y CDNs aplican sin pedirte permiso.

    Ahí es donde la mayoría termina en POST. Sin límite de tamaño relevante, acepta cualquier estructura en el body. Pero POST miente.

    Le dice a cualquier intermediario —proxy, CDN, gateway— "esto modifica el estado del servidor, no lo cachees". Aunque tu POST /contacts/search solo esté leyendo datos.

    El resultado: pierdes cacheo, pierdes la garantía de idempotencia que un retry automático podría necesitar, y terminas inventando convenciones como POST /contacts/_query para comunicar, solo con el nombre de la ruta y no con el protocolo, que en realidad es una lectura.

    Yo terminé documentando en el README: "este POST es en realidad una consulta de solo lectura". Un parche humano para un problema que el protocolo debería resolver solo.


    Cómo llegamos hasta aquí

    En corto: HTTP no tuvo, durante veinte años, un método pensado para consultas complejas que fuera a la vez seguro, idempotente y cacheable — y la industria lo parcheó de mil formas distintas hasta que el RFC 10008 lo resolvió en 2026.

    Este problema no es nuevo. Es viejo.

    Las URIs de GET siempre tuvieron un techo práctico. No existe un límite en el estándar HTTP, pero servidores, proxies y navegadores lo imponen igual —y con diez o quince filtros combinables, lo tocas rápido.

    La industria hizo lo que hace siempre ante un vacío del protocolo: usar POST para todo lo que GET no aguantaba. Búsquedas complejas, filtros anidados, exportaciones con parámetros, todo empaquetado en un body, aunque la operación fuera, en esencia, una lectura.

    El coste de ese abuso semántico es real. Rompe el cacheo, porque los intermediarios no cachean POST por defecto. Rompe la idempotencia garantizada, porque un cliente no puede asumir que reintentar un POST es seguro. Y confunde a cualquier proxy o CDN que tome decisiones basadas en el método HTTP.

    Hubo intentos de arreglarlo antes de 2026. WebDAV definió su propio método, SEARCH (RFC 5323), pensado para consultas complejas sobre colecciones de recursos. Nunca salió de su nicho.

    Mientras tanto, herramientas que viven de resolver búsquedas complejas todos los días —Elasticsearch es el ejemplo obvio— no adoptaron SEARCH. Optaron por su propia convención, POST /_search, aceptando el mismo trade-off semántico que cualquiera de nosotros.

    Veinte años de parches, cada uno resolviendo el síntoma, ninguno el problema de fondo: HTTP no tenía un método pensado para "quiero enviarte una consulta compleja, y quiero que sepas que es segura, idempotente y cacheable".

    En 2026 el IETF lo estandarizó. El RFC 10008 —de Julian Reschke, James M. Snell y Mike Bishop, publicado como Proposed Standard— define el método QUERY exactamente para esto.


    Qué es el método HTTP QUERY y cómo funciona

    El RFC lo resume mejor que yo (traducción propia del original en inglés):

    "El input de la operación query se pasa como contenido de la petición en vez de como parte de la URI de la petición. A diferencia de POST, sin embargo, el método es explícitamente seguro e idempotente."

    QUERY toma la ventaja práctica de POST —el body, sin límites de tamaño relevantes, con soporte para estructuras complejas— y le devuelve las tres garantías semánticas que POST no ofrece:

    • Seguro. No modifica el estado del recurso; un intermediario puede asumir que ejecutar la petición no tiene efectos secundarios.
    • Idempotente. Puedes reintentar la misma petición todas las veces que necesites sin miedo a duplicar nada.
    • Cacheable. La respuesta puede cachearse siguiendo las reglas estándar de HTTP caching, igual que un GET.

    No es un detalle cosmético. Es lo que le permite a un proxy o una CDN cachear agresivamente sin arriesgarse a servir datos corruptos, porque el propio protocolo garantiza que la operación es de solo lectura.

    Así se ve una petición QUERY, tal cual la define el RFC:

    QUERY /contacts HTTP/1.1
    Host: example.org
    Content-Type: application/x-www-form-urlencoded
    Accept: application/json
    
    select=surname,givenname&limit=10&match="email=*@example.*"
    

    Línea por línea

    • QUERY /contacts — el verbo nuevo, apuntando al recurso de colección, igual que harías con GET.
    • Content-Type — obligatorio. El servidor DEBE fallar la petición si el header falta o es inconsistente con el contenido real del body.
    • Accept — content negotiation estándar para la respuesta.
    • El body —select, limit, match— es donde vive la complejidad real de tu consulta, sin límites de URI ni arrays serializados en un query string.

    (El body de este ejemplo está simplificado por legibilidad — en una petición application/x-www-form-urlencoded real, esos valores llevarían percent-encoding.)

    El manejo de errores no deja ambigüedad. Si la petición no trae información suficiente sobre el media type, el servidor responde 400 Bad Request. Si el media type está identificado pero no es soportado, responde 415 Unsupported Media Type.

    QUERY también soporta los condicionales HTTP que ya conoces —If-Modified-Since, If-None-Match— para re-consultar de forma eficiente sin traer de vuelta una respuesta que no cambió.

    Si en tu backend ya validas y tipas los bodies de entrada, la lógica no cambia con QUERY: sigues necesitando un schema que valide select, limit y match antes de tocar tu capa de datos. QUERY no te libra de validar el input, solo te da un protocolo que comunica correctamente la intención. En el curso de Zod cubrimos justo este tipo de validación de bodies complejos con schemas tipados en TypeScript.


    Cacheo y content negotiation: la parte que cambia las reglas

    Con GET, la cache key es la URL. Punto.

    Con QUERY no puede serlo, porque la consulta vive en el body. El RFC lo resuelve así: la cache key DEBE incorporar el contenido de la petición y su metadata relacionada. Las caches, eso sí, pueden normalizar diferencias semánticamente insignificantes —encoding, formato JSON con espacios distintos— para no fragmentar el cacheo por diferencias triviales.

    Para que un recurso anuncie qué formatos de consulta soporta existe el header de respuesta Accept-Query, con sintaxis de Structured Fields. Es el equivalente a un Accept, pero para las capacidades de consulta del propio recurso.

    Hay un detalle elegante más. Una respuesta 2xx a una QUERY puede incluir los headers Location o Content-Location apuntando a una URI equivalente:

    Location: /contacts/stored-queries/42
    Content-Location: /contacts/stored-results/17
    

    Eso te permite guardar esa consulta —o su resultado— como un recurso direccionable por GET, sin reenviar el body completo cada vez que alguien quiera acceder al mismo resultado. El RFC es explícito en que esas URIs deberían elegirse de forma que no incluyan partes sensibles del contenido original de la petición.


    El detalle que se te va a escapar: CORS

    Esto es lo que casi nadie menciona cuando lee sobre QUERY por encima, y es justo lo que te va a morder si construyes APIs consumidas desde un frontend.

    GET, POST y HEAD están en la lista de métodos "CORS-safelisted": el navegador puede dispararlos cross-origin sin pedir permiso primero. QUERY no está en esa lista.

    Cualquier petición QUERY cross-origin dispara automáticamente un preflight: una petición OPTIONS previa donde el navegador le pregunta al servidor "¿me dejas hacer esto?" antes de ejecutar la petición real.

    No es un bug ni una limitación del RFC. Es una decisión de seguridad del propio modelo CORS. Si vas a exponer un endpoint QUERY consumido desde un dominio distinto al de tu API, necesitas tener el preflight resuelto en tu configuración de CORS, o vas a ver peticiones fallando sin entender por qué — el día que tu stack te deje disparar una petición QUERY real desde el navegador. Ese nivel de soporte todavía no lo puedo confirmar, como explico un poco más abajo.


    Qué significa esto en la práctica, hoy

    En corto: sí puedes diseñar tu API con la semántica de QUERY desde ya, aunque el transporte real siga siendo POST mientras el soporte nativo del ecosistema madura.

    Aquí toca ser honesto.

    El RFC 10008 se publicó en 2026. Es un Proposed Standard del IETF —el sello más alto para un método nuevo— pero eso no significa que el ecosistema ya lo soporte de forma nativa en todas partes.

    No tengo forma de confirmar, a la fecha de este post, qué nivel de soporte real tienen ya el fetch() de los navegadores o los frameworks de backend en Node —Express, NestJS, Hono— para este método. Es un estándar muy reciente y ese tipo de soporte cambia semana a semana. No me voy a inventar un dato que no puedo verificar.

    Lo que sí puedes hacer hoy, con certeza, es diseñar tus endpoints con el modelo semántico correcto. Aunque el transporte real siga siendo POST por compatibilidad, puedes:

    1. Documentar que tu endpoint de búsqueda es una operación segura e idempotente, aunque use el verbo POST.
    2. Construir la cache key de tu capa de caché —Redis, CDN, lo que uses— incorporando el body completo, el mismo principio que usa QUERY.
    3. Exponer resultados reutilizables vía una URI propia, tu propio Content-Location casero, para que un cliente pueda hacer GET después sin repetir la consulta.

    Ese diseño no caduca. El día que tu framework soporte QUERY de forma nativa, migrar es un cambio de un verbo, porque la arquitectura ya estaba pensada correctamente.

    Si tu backend está en NestJS, esta es exactamente el tipo de decisión de diseño de API que vale la pena resolver bien desde el controller. En el post sobre streaming con NestJS y el AI SDK de Vercel hablo de cómo estructurar endpoints que respetan la semántica HTTP correcta en vez de forzar todo por POST.

    Del lado del frontend, si consumes estos endpoints desde Angular, la resource API introducida en v22 encaja con este modelo: una consulta segura y cacheable es exactamente el tipo de dato que quieres modelar como un resource reactivo, no como un efecto secundario disparado a mano. Lo cubro en el post sobre la resource API en Angular 22, y trabajamos el consumo de APIs con el Angular moderno —signals, resource, control flow— en el curso de Angular Moderno.

    Hay un ángulo más que me parece el más interesante, y casi nadie lo está conectando todavía. Los agentes de IA que hacen tool-calling —vía MCP o cualquier otro protocolo— tienen el mismo problema que resolvimos hace veinte años con las APIs REST: un agente necesita saber, con certeza protocolar, si una tool que va a invocar es segura de reintentar o no.

    QUERY le da a ese tipo de arquitecturas una semántica formal para "esto es una consulta, puedes cachearla, puedes reintentarla sin miedo". Es exactamente el tipo de diseño de herramientas que trabajamos en el curso de Construye con IA: que cada tool que expones a un agente tenga una semántica clara sobre sus efectos.


    GET vs QUERY vs POST, en una tabla

    Aspecto GET QUERY POST
    Seguro Potencialmente no
    Idempotente Potencialmente no
    Query en la URI Opcional* No
    Cacheable Sí (limitado)**

    * Que el protocolo lo permita no significa que sea buena práctica: si vuelves a meter toda la consulta en la URI, pierdes la ventaja que motivó usar QUERY en primer lugar.

    ** Solo con headers de cache explícitos configurados a mano — no por defecto, como sí ocurre con GET y QUERY.


    La tesis: esto no es una feature exótica

    Llevábamos más de veinte años sin una respuesta oficial en el protocolo HTTP a una pregunta simple: ¿cómo hago una consulta compleja de forma segura, cacheable e idempotente?

    No es que nadie lo necesitara. Es que cada quien lo parcheaba a su manera —convenciones de nombres, métodos no estándar, documentación humana explicando lo que el protocolo no podía comunicar solo.

    QUERY no es HTTP inventando una feature exótica. Es HTTP poniéndose al día con un patrón que la industria ya necesitaba y ya estaba resolviendo, mal, de mil formas distintas.

    Y esa es la parte que importa para tu trabajo diario: entender bien la semántica HTTP —qué es seguro, qué es idempotente, qué es cacheable— es una habilidad de arquitectura que trasciende cualquier framework. Angular, NestJS, Express, Hono van a cambiar. Los verbos y garantías de HTTP, no tanto.


    Preguntas frecuentes sobre el método HTTP QUERY

    ¿Qué es el método HTTP QUERY?

    Es un método HTTP nuevo, estandarizado en el RFC 10008 (IETF, Proposed Standard, 2026) por Julian Reschke, James M. Snell y Mike Bishop. Permite enviar el input de una consulta como contenido de la petición en vez de codificarlo en la URI, y a diferencia de POST, es explícitamente seguro, idempotente y cacheable.

    ¿QUERY reemplaza a POST para hacer búsquedas?

    Reemplaza el uso de POST para operaciones de lectura que necesitan un body complejo: búsquedas, filtros combinados, consultas estructuradas. POST sigue siendo correcto para operaciones que sí modifican estado. El problema que QUERY resuelve es el abuso semántico de usar POST para leer datos, no el uso legítimo de POST para escribir.

    ¿Cuál es la diferencia entre el método QUERY y POST en HTTP?

    La diferencia no es de capacidad —ambos aceptan un body con estructuras complejas— sino de las garantías que cada método comunica al resto de la infraestructura HTTP. POST no promete que la operación sea segura ni idempotente, así que ningún proxy o CDN puede asumirlo ni cachearla por defecto. QUERY sí lo garantiza explícitamente: es seguro, idempotente y cacheable, igual que GET, pero sin los límites de una URI.

    ¿Ya puedo usar el método QUERY en producción hoy?

    Con cautela. El RFC se publicó en 2026 y es muy reciente —no hay forma de confirmar en este momento qué nivel de soporte nativo tienen ya los navegadores (fetch()) o los frameworks de backend más usados en Node. Lo prudente es diseñar tus endpoints con la semántica correcta de QUERY aunque sigas transportándolos con POST mientras el soporte nativo del ecosistema madura.

    ¿Cómo se cachea una petición QUERY si la consulta no está en la URL?

    La cache key deja de basarse solo en la URL, como con GET, y debe incorporar el contenido completo de la petición y su metadata relacionada. Las caches pueden normalizar diferencias semánticamente insignificantes —como el encoding o el formato del JSON— para no fragmentar el cacheo innecesariamente.

    ¿Qué diferencia hay entre QUERY y el método SEARCH de WebDAV?

    SEARCH (RFC 5323) fue un intento anterior, específico de WebDAV, para consultas complejas sobre colecciones de recursos, y nunca tuvo adopción fuera de ese nicho. QUERY es un método de propósito general, estandarizado en el núcleo de HTTP —no atado a una extensión como WebDAV—, con reglas explícitas de content negotiation, cacheo y manejo de errores que SEARCH nunca definió con ese nivel de detalle.

    ¿Por qué una petición QUERY cross-origin necesita un preflight?

    Porque QUERY no está en la lista de métodos "CORS-safelisted", a diferencia de GET, POST y HEAD. Cualquier método fuera de esa lista obliga al navegador a enviar una petición OPTIONS previa —el preflight— para confirmar que el servidor permite esa petición antes de ejecutarla.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • De callbacks a Signals: la reactividad real del frontend

    De callbacks a Signals: la reactividad real del frontend

    Un excliente me escribió hace años, angustiado. Su carrito de compras mostraba 3 artículos en el header, pero el checkout decía que había 5. Los clientes se quejaban en soporte y algunos abandonaban la compra.

    Revisé el código. Puro estilo jQuery: DOM manipulado a mano, evento por evento. Un event listener actualizaba el contador del header. Otro, completamente separado, actualizaba el resumen del checkout. Nadie los había conectado entre sí — y ahí estaba el problema real: cero programación reactiva, cero garantía de que el estado y la interfaz dijeran la misma verdad.

    Cuando alguien hacía clic dos veces seguidas y rápido, un listener terminaba antes que el otro. El total quedaba repartido entre cuatro variables sueltas, cada una con su propia versión de la verdad. Pasé tres horas arreglando algo que debería haberme tomado diez minutos. No porque el bug fuera complejo — porque nada en el código garantizaba que la interfaz reflejara el estado real.

    Llevamos veinte años resolviendo ese mismo problema con herramientas distintas. Primero fueron callbacks manuales sobre el DOM. Luego llegó el Virtual DOM. Ahora, señales. Cada era resolvió lo que la anterior no pudo — y entender por qué importa más que memorizar sintaxis nueva cada dos años.


    Era 1: callbacks manuales y el DOM que se te olvida sincronizar

    En los tiempos de jQuery — y del DOM vanilla antes de eso — la única forma de reaccionar a un evento era escucharlo y mutar el DOM a mano. Tú decidías qué elemento tocar, cuándo y con qué valor.

    Toma el ejemplo clásico: un contador de carrito con tres elementos que dependen del mismo dato.

    let count = 0;
    
    const counterEl = document.querySelector('#counter');
    const totalEl = document.querySelector('#total');
    const shippingMsgEl = document.querySelector('#shipping-msg');
    
    document.querySelector('#add-btn').addEventListener('click', () => {
      count++;
      counterEl.textContent = count;
      totalEl.textContent = `$${(count * 19.99).toFixed(2)}`;
      shippingMsgEl.textContent = count >= 5
        ? '¡Envío gratis!'
        : `Añade ${5 - count} más para envío gratis`;
    });
    
    document.querySelector('#remove-btn').addEventListener('click', () => {
      count = Math.max(0, count - 1);
      counterEl.textContent = count;
      totalEl.textContent = `$${(count * 19.99).toFixed(2)}`;
      // shippingMsgEl no se actualiza aquí. Nadie lo notó en code review.
    });
    

    Mira el comentario en la última línea. Ese es, casi literal, el bug que revisé en el carrito de mi excliente.

    No es un error de sintaxis — el código compila, pasa QA si nadie prueba el camino de "quitar un producto cuando ya tenías envío gratis". El bug vive en la cabeza del developer: hay que acordarse de tocar los tres elementos en cada handler que mueva ese estado.

    La ventaja de este modelo es real: control total, cero abstracciones, cero curva de aprendizaje. Para un widget aislado — un acordeón, un modal, un tooltip — sigue siendo la opción correcta hoy mismo.

    El problema aparece en cuanto el estado deja de ser trivial:

    • Cada elemento dependiente necesita su propia línea de sincronización, repetida en cada handler que toque ese estado.
    • El estado vive disperso: a veces en el DOM (el.textContent), a veces en variables sueltas, a veces en atributos data-*.
    • Los listeners no se limpian solos. En una SPA que monta y desmonta vistas, cada addEventListener sin su removeEventListener es un memory leak esperando a pasar factura.

    Esto nunca fue un problema de jQuery. Fue un problema de arquitectura: nada en el modelo te obligaba a centralizar el estado ni a declarar sus dependencias. Cada developer inventaba su propia disciplina — y la disciplina, a escala de equipo, no escala.

    Era 2: Virtual DOM y el modelo declarativo

    React cambió la pregunta. En lugar de "¿qué elemento del DOM tengo que tocar?", pasó a ser "¿cómo se ve la UI dado este estado?". Tú describes el resultado final; el framework decide cómo llegar ahí.

    function Counter() {
      const [count, setCount] = useState(0);
      const total = (count * 19.99).toFixed(2);
      const shippingMsg = count >= 5
        ? '¡Envío gratis!'
        : `Añade ${5 - count} más para envío gratis`;
    
      return (
        <div>
          <p>{count}</p>
          <p>${total}</p>
          <p>{shippingMsg}</p>
          <button onClick={() => setCount(c => c + 1)}>Añadir</button>
          <button onClick={() => setCount(c => Math.max(0, c - 1))}>Quitar</button>
        </div>
      );
    }
    

    El bug del carrito es estructuralmente imposible aquí. total y shippingMsg se calculan en la misma función, a partir del mismo count, cada vez que el componente se ejecuta. No hay "actualizar" — hay "recalcular todo desde cero", así que no hay forma de que uno se sincronice y el otro se olvide.

    Ahí está la clave del Virtual DOM. React no toca el DOM real en cada cambio. Construye un árbol en memoria — objetos JavaScript planos que describen cómo debería verse la UI — y lo compara contra el árbol anterior. Ese proceso se llama reconciliation, y el algoritmo de comparación es el diffing: detecta qué nodos cambiaron, cuáles se reutilizan, y calcula el mínimo de operaciones para que el DOM real refleje el nuevo árbol. Solo entonces toca el DOM — y solo donde hace falta.

    Es un modelo declarativo y predecible. Pero el coste real no es gratis, y es lo que casi nadie menciona en los tutoriales de introducción: cada cambio de estado re-ejecuta la función completa del componente y, por defecto, la de sus hijos.

    En un árbol de cuarenta componentes anidados, un solo tecleo puede disparar cuarenta re-renders y cuarenta diffs — la mayoría comparando nodos que ni siquiera cambiaron.

    La respuesta del ecosistema fue la memoization: memo(), useMemo(), useCallback(). Son parches necesarios para un problema que el propio modelo introduce: no sabes qué cambió hasta que recalculas y comparas. Memoizar es responsabilidad manual otra vez — la misma que el Virtual DOM prometía eliminar, solo que movida un nivel más arriba en el árbol.

    Era 3: reactividad fina — el grafo en vez del árbol

    Los signals no comparan nada. No hay árbol virtual, no hay diffing, no hay re-render de una función completa. Un signal es una caja que guarda un valor y sabe, con precisión, quién depende de él.

    import { Component, signal, computed, effect } from '@angular/core';
    
    @Component({
      selector: 'app-cart-counter',
      template: `
        <p>{{ count() }}</p>
        <p>${{ total() }}</p>
        <p>{{ shippingMsg() }}</p>
        <button (click)="count.set(count() + 1)">Añadir</button>
        <button (click)="count.set(count() - 1)">Quitar</button>
      `,
    })
    export class CartCounterComponent {
      count = signal(0);
    
      total = computed(() => (this.count() * 19.99).toFixed(2));
    
      shippingMsg = computed(() =>
        this.count() >= 5
          ? '¡Envío gratis!'
          : `Añade ${5 - this.count()} más para envío gratis`
      );
    
      constructor() {
        effect(() => {
          console.log(`Carrito: ${this.count()} items — $${this.total()}`);
        });
      }
    }
    

    Cuando count cambia, Angular no re-ejecuta el componente entero ni reconstruye ningún árbol para comparar. total y shippingMsg ya saben que dependen de count — lo registraron la primera vez que se ejecutaron, al construirse el grafo reactivo. Angular actualiza exactamente el nodo del DOM ligado a cada binding. Nada más se mueve.

    Esto es reactividad fina (fine-grained reactivity): la granularidad de la actualización no es el componente, ni el subárbol — es el binding individual. Angular v22 lleva esto hasta el final siendo zoneless por defecto: ya no depende de Zone.js interceptando cada setTimeout o evento del navegador para saber cuándo revisar cambios. El grafo de signals es la única fuente de verdad sobre qué actualizar y cuándo.

    Angular no inventó este modelo — lo adoptó y lo llevó a producción a escala. Solid.js lo demostró primero, sin Virtual DOM desde el diseño inicial. Svelte llega a un resultado parecido compilando la reactividad en tiempo de build. Los tres coinciden en el mismo diagnóstico: comparar árboles es trabajo evitable si sabes de antemano quién depende de quién.

    Si quieres ver cada primitiva documentada en detalle, la guía oficial de Angular Signals cubre signal(), computed() y effect() con más profundidad de la que cabe en un post.

    Si quieres ver cómo se construye ese grafo de dependencias paso a paso — incluyendo los casos raros donde un effect() se dispara más veces de las que esperas — lo cubrí a fondo en el post sobre el grafo reactivo de Angular Signals.

    En el curso de Angular Moderno construimos este modelo mental desde cero, con proyectos reales donde pasar de Zone.js a zoneless cambia decisiones de arquitectura, no solo de sintaxis.

    Los tres paradigmas, uno al lado del otro

    Modelo mental Cómo detecta cambios Granularidad de la actualización Coste computacional Dónde brilla
    Callbacks (jQuery / DOM imperativo) Tú mutas el DOM a mano, evento por evento No detecta nada — el developer decide cuándo actualizar La que tú programes, elemento por elemento Bajo por operación, alto en mantenimiento y bugs de sincronización Widgets aislados, prototipos, páginas sin estado compartido
    Virtual DOM (React) La UI es una función pura del estado Diffing — compara árbol virtual anterior vs. nuevo Por componente/subárbol, tras re-ejecutar y comparar Re-ejecuta la función de render completa y diffea en cada cambio Apps con estado complejo, equipos grandes, ecosistema maduro
    Signals (Angular, Solid, Svelte) Grafo de dependencias reactivas Suscripción directa — el signal sabe quién lo consume El binding o nodo exacto del DOM que depende del valor Solo se ejecuta lo que realmente cambió UI de alta frecuencia de actualización, listas grandes, apps sensibles a rendimiento

    Por qué la reactividad fina no es una moda

    Cada era resolvió el cuello de botella real de la anterior — no la anterior en abstracto, la anterior en producción.

    Los callbacks resolvieron "cómo reacciono a un evento del usuario". Fue suficiente mientras la UI tenía poco estado compartido. Dejó de serlo en cuanto una sola acción tenía que actualizar cinco sitios distintos de la pantalla.

    El Virtual DOM resolvió "cómo mantengo la UI declarativa sin perder la cordura sincronizando elementos a mano". A cambio, aceptó un coste: recalcular y comparar árboles que, la mayoría de las veces, apenas habían cambiado.

    Signals resuelve el cuello de botella que el Virtual DOM introdujo: cómo evitar recalcular y comparar lo que ya sabías que no había cambiado. No es una versión "más rápida" de React. Es una respuesta distinta a la misma pregunta de fondo: ¿qué es lo mínimo que tengo que actualizar para que la UI diga la verdad?

    Esto no significa que el Virtual DOM esté acabado, ni que debas reescribir tu app de React mañana.

    Significa que si estás arrancando un proyecto hoy, entender este modelo ya no es opcional — es la diferencia entre construir sobre un patrón que resuelve el problema en su raíz o sobre uno que lo parchea con memoization.

    Esta decisión de arquitectura — dónde vive el estado, cómo fluye, qué parte del sistema es responsable de mantenerlo sincronizado con la UI — es exactamente el tipo de decisión que trato en el post sobre Clean Architecture para frontend con IA: la reactividad que elijas no es un detalle de implementación, es una decisión que carga con consecuencias durante años.

    Si vas a construir con signals en producción, en algún momento necesitarás verificar que esos computed() y effect() se comportan como esperas bajo distintos escenarios — eso es justo lo que trabajamos con casos reales en el curso de Testing en Angular con Jest y Testing Library.

    Y si quieres discutir esto con otros developers que están tomando las mismas decisiones ahora mismo, en Dominicode Labs es donde pasa esa conversación cada semana.

    Preguntas frecuentes sobre programación reactiva en el frontend

    ¿Qué es la programación reactiva?

    Es el paradigma en el que la interfaz se actualiza automáticamente cuando cambia el estado del que depende, sin que el desarrollador tenga que sincronizarla a mano evento por evento. Los tres modelos de este post — callbacks, Virtual DOM y signals — son formas distintas de resolver ese mismo problema, con más o menos reactividad real incorporada al framework.

    ¿El Virtual DOM está muerto?

    No. Sigue siendo el modelo dominante en producción — React tiene el ecosistema, el talento disponible y millones de líneas de código funcionando con él hoy. Lo que cambió es que ya no es la única opción seria para UI compleja: Signals, Solid.js y Svelte demuestran que el diffing es una solución al problema, no la única posible.

    ¿Los Signals reemplazan a React?

    No en el sentido de que React vaya a desaparecer. Angular con Signals, Solid.js y Svelte son alternativas con un modelo distinto, no reemplazos del ecosistema React. Sí es cierto que la presión competitiva ya empujó a React hacia herramientas como React Compiler, que intenta automatizar la memoization que antes hacías a mano.

    ¿Qué es la reactividad fina (fine-grained reactivity)?

    Es un modelo donde cada pieza de estado (signal) mantiene una lista explícita de quién depende de ella — otros signals derivados (computed) o efectos secundarios (effect). Cuando el valor cambia, solo se re-ejecuta lo que está suscrito a ese valor específico, sin comparar árboles ni recalcular lo que no depende de ese dato.

    ¿Angular usa Virtual DOM?

    No, y nunca lo usó. Angular usaba Zone.js y un mecanismo de change detection basado en recorrer el árbol de componentes buscando cambios. Con Signals y el modo zoneless, por defecto desde Angular v22, Angular elimina también ese recorrido: el grafo de signals le dice exactamente qué actualizar, sin Zone.js y sin diffing.

    ¿Debo migrar mi app de React a Signals?

    No si tu app funciona bien y el equipo domina React. La reactividad fina brilla en escenarios concretos: dashboards con actualizaciones muy frecuentes, listas grandes, apps donde el rendimiento de render es un cuello de botella medido, no sospechado. Si estás empezando un proyecto nuevo, sí vale la pena evaluar Angular v22 con Signals como opción seria.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • NestJS + Vercel AI SDK: backend streaming IA en producción

    NestJS + Vercel AI SDK: backend streaming IA en producción

    ANTHROPIC_API_KEY=sk-ant-xxxxxxxx

    
    En `app.module.ts`, registra `ConfigModule`:
    
    ```typescript
    // src/app.module.ts
    import { Module } from '@nestjs/common';
    import { ConfigModule } from '@nestjs/config';
    import { AiModule } from './ai/ai.module';
    
    @Module({
      imports: [
        ConfigModule.forRoot({ isGlobal: true }),
        AiModule,
      ],
    })
    export class AppModule {}
    

    isGlobal: true significa que ConfigService está disponible en todos los módulos sin importarlo individualmente. Práctico.


    La estructura del AiModule

    Antes de escribir código, la estructura:

    src/
      ai/
        ai.module.ts
        ai.controller.ts
        ai.service.ts
        dto/
          chat.dto.ts
    

    Cuatro archivos. Eso es todo lo que necesita un endpoint de streaming limpio.


    Paso 1: El DTO de validación

    El primer punto de defensa es el DTO. Define el contrato del request:

    // src/ai/dto/chat.dto.ts
    import { IsArray, IsIn, IsString, ValidateNested, ArrayMinSize } from 'class-validator';
    import { Type } from 'class-transformer';
    
    export class ChatMessageDto {
      @IsIn(['user', 'assistant', 'system'])
      role: 'user' | 'assistant' | 'system';
    
      @IsString()
      content: string;
    }
    
    export class ChatRequestDto {
      @IsArray()
      @ArrayMinSize(1)
      @ValidateNested({ each: true })
      @Type(() => ChatMessageDto)
      messages: ChatMessageDto[];
    }
    

    @ValidateNested({ each: true }) valida cada elemento del array individualmente. Si el frontend manda un mensaje con role: 'hacker' o sin content, el request rebota antes de tocar el servicio.

    Para que ValidationPipe funcione globalmente, añádelo en main.ts:

    // src/main.ts
    import { NestFactory } from '@nestjs/core';
    import { ValidationPipe } from '@nestjs/common';
    import { AppModule } from './app.module';
    
    async function bootstrap() {
      const app = NestFactory.create(AppModule);
    
      app.useGlobalPipes(new ValidationPipe({
        transform: true,
        whitelist: true,    // elimina propiedades no declaradas en el DTO
        forbidNonWhitelisted: true,
      }));
    
      // CORS para el frontend Angular en desarrollo
      app.enableCors({
        origin: process.env.FRONTEND_URL ?? 'http://localhost:4200',
        methods: ['POST', 'OPTIONS'],
      });
    
      await app.listen(process.env.PORT ?? 3000);
    }
    
    bootstrap();
    

    whitelist: true es especialmente importante aquí: elimina cualquier campo del body que no esté declarado en el DTO. Si alguien intenta inyectar propiedades extra en el request, NestJS las ignora antes de que lleguen al servicio.


    Paso 2: El AiService

    El servicio encapsula toda la lógica de llamada al modelo. El controlador no sabe qué modelo usamos ni cómo se configura — solo llama al servicio y recibe el stream.

    // src/ai/ai.service.ts
    import { Injectable } from '@nestjs/common';
    import { ConfigService } from '@nestjs/config';
    import { streamText, CoreMessage } from 'ai';
    import { createAnthropic } from '@ai-sdk/anthropic';
    
    @Injectable()
    export class AiService {
      private readonly anthropic;
    
      constructor(private readonly config: ConfigService) {
        this.anthropic = createAnthropic({
          apiKey: this.config.getOrThrow<string>('ANTHROPIC_API_KEY'),
        });
      }
    
      streamChat(messages: CoreMessage[]) {
        return streamText({
          model: this.anthropic('claude-sonnet-4-6'),
          system: `Eres un asistente técnico especializado en desarrollo de software.
    Responde en español de forma concisa y directa.
    Si el usuario pregunta sobre código, incluye ejemplos concretos.`,
          messages,
          maxTokens: 1024,
        });
      }
    }
    

    Dos decisiones importantes aquí:

    createAnthropic({ apiKey }) en el constructor — el cliente de Anthropic se crea una sola vez cuando NestJS instancia el servicio. No se recrea en cada petición. Eso evita overhead innecesario.

    config.getOrThrow<string>('ANTHROPIC_API_KEY') — si la variable de entorno no existe, la app falla en el arranque con un error claro en lugar de fallar silenciosamente en el primer request. Fail fast.

    maxTokens: 1024 es un límite defensivo. Sin él, un usuario puede hacer una pregunta que genere una respuesta de 8.000 tokens, multiplicando el costo por 8. Ajusta según tu caso de uso.


    Paso 3: El AiController con streaming

    El controlador es donde ocurre la magia del streaming. La clave está en cómo NestJS maneja la respuesta HTTP nativa:

    // src/ai/ai.controller.ts
    import {
      Controller,
      Post,
      Body,
      Res,
      HttpCode,
      HttpStatus,
    } from '@nestjs/common';
    import { Response } from 'express';
    import { AiService } from './ai.service';
    import { ChatRequestDto } from './dto/chat.dto';
    import { CoreMessage } from 'ai';
    
    @Controller('api')
    export class AiController {
      constructor(private readonly aiService: AiService) {}
    
      @Post('chat')
      @HttpCode(HttpStatus.OK)
      async chat(
        @Body() body: ChatRequestDto,
        @Res() res: Response,
      ): Promise<void> {
        const messages = body.messages as CoreMessage[];
    
        const result = this.aiService.streamChat(messages);
    
        // toUIMessageStreamResponse() genera una Response Web estándar
        // con el protocolo SSE del AI SDK
        const streamResponse = result.toUIMessageStreamResponse();
    
        // Propagamos los headers del AI SDK a la respuesta de Express
        streamResponse.headers.forEach((value, key) => {
          res.setHeader(key, value);
        });
    
        res.status(streamResponse.status);
    
        // Volcamos el body del ReadableStream a la respuesta de Express
        if (streamResponse.body) {
          const reader = streamResponse.body.getReader();
    
          const pump = async () => {
            while (true) {
              const { done, value } = await reader.read();
              if (done) {
                res.end();
                break;
              }
              res.write(value);
            }
          };
    
          pump().catch((err) => {
            console.error('[AiController] Error en stream:', err);
            if (!res.headersSent) {
              res.status(500).json({ error: 'Error interno del stream' });
            } else {
              res.end();
            }
          });
        } else {
          res.status(500).json({ error: 'No se pudo iniciar el stream' });
        }
      }
    }
    

    ¿Por qué este patrón de pump manual en lugar de pipe()?

    toUIMessageStreamResponse() devuelve una Response Web estándar (la del spec WHATWG), no un stream de Node.js. Express trabaja con streams de Node.js. El pump manual convierte uno en el otro sin dependencias adicionales. Es verboso pero explícito — sabes exactamente qué hace cada línea.

    El bloque catch en el pump gestiona dos escenarios: si el error ocurre antes de enviar headers, devuelve un 500 con JSON. Si ocurre después (cuando el stream ya está activo), llama a res.end() para cerrar la conexión limpiamente. Sin este manejo, el cliente se quedaría esperando indefinidamente.


    Paso 4: El AiModule

    El módulo agrupa las tres piezas:

    // src/ai/ai.module.ts
    import { Module } from '@nestjs/common';
    import { AiController } from './ai.controller';
    import { AiService } from './ai.service';
    
    @Module({
      controllers: [AiController],
      providers: [AiService],
      exports: [AiService], // por si otros módulos necesitan AiService
    })
    export class AiModule {}
    

    Exportar AiService es una decisión de diseño: si en el futuro un módulo de AgentsModule o DocumentModule necesita llamar al modelo, importan AiModule y tienen el servicio disponible sin duplicar configuración.


    Rate limiting: el paso que nadie incluye

    Sin rate limiting, un solo usuario puede vaciar tu cuota de Anthropic en minutos. NestJS tiene @nestjs/throttler para esto:

    npm install @nestjs/throttler
    

    Configúralo en AppModule:

    // src/app.module.ts
    import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
    import { APP_GUARD } from '@nestjs/core';
    
    @Module({
      imports: [
        ConfigModule.forRoot({ isGlobal: true }),
        ThrottlerModule.forRoot([{
          name: 'short',
          ttl: 60_000,   // 1 minuto en ms
          limit: 10,     // máximo 10 requests por minuto por IP
        }]),
        AiModule,
      ],
      providers: [
        {
          provide: APP_GUARD,
          useClass: ThrottlerGuard,
        },
      ],
    })
    export class AppModule {}
    

    10 requests por minuto por IP es un límite conservador para un chat. En producción, ajusta según el plan de Anthropic que tengas y el perfil de uso esperado. Si tus usuarios son developers que mandan snippets de código largos, 10 puede ser demasiado restrictivo. Si es un chat de soporte con usuarios anónimos, puede ser demasiado permisivo.

    ThrottlerGuard como APP_GUARD aplica el límite a todos los endpoints automáticamente. Si quieres excluir algunos endpoints del límite, usa el decorador @SkipThrottle() en el controlador correspondiente.


    Conectar con el frontend Angular

    Este backend está diseñado para ser el complemento del post Angular v22 + Vercel AI SDK: streaming de IA en tu app en 20 minutos.

    El frontend Angular usa fetch nativo con ReadableStream. El cambio que necesitas en el componente Angular es mínimo: actualizar la URL del endpoint del servidor Bun del post anterior (típicamente en el puerto 4000) a http://localhost:3000/api/chat de este servidor NestJS. El contrato del API no cambia — misma ruta, mismo formato de mensajes.

    La diferencia está en el protocolo de stream. El servidor Bun del post anterior usa toTextStreamResponse(), que devuelve texto plano. Este NestJS usa toUIMessageStreamResponse(), que usa el protocolo SSE estructurado del AI SDK. Para consumir este protocolo desde Angular sin la librería useChat de React, el componente Angular necesita parsear los chunks SSE en lugar de concatenarlos directamente.

    Si ya tienes el frontend del post anterior y quieres migrar a este backend sin tocar el componente, cambia en AiService.streamChat() el retorno a toTextStreamResponse():

    // AiService — variante compatible con el componente Angular del post anterior
    streamChat(messages: CoreMessage[]) {
      return streamText({
        model: this.anthropic('claude-sonnet-4-6'),
        system: 'Eres un asistente técnico...',
        messages,
        maxTokens: 1024,
      });
      // En el controlador usar toTextStreamResponse() en vez de toUIMessageStreamResponse()
    }
    

    Y en el controlador, sustituye result.toUIMessageStreamResponse() por result.toTextStreamResponse(). El componente Angular del post anterior funciona sin cambios.

    La versión con toUIMessageStreamResponse() es la recomendada para proyectos nuevos porque soporta tool calls, metadatos de uso de tokens, y datos personalizados dentro del mismo stream — funcionalidades que toTextStreamResponse() no puede transmitir.

    Característica toUIMessageStreamResponse() toTextStreamResponse()
    Protocolo AI SDK SSE estructurado Texto plano
    Tool calls
    Metadatos de tokens
    Compatible con useChat
    Parsing manual en cliente Necesario sin useChat No necesario
    Cuándo usarlo Proyectos nuevos Compatibilidad con cliente simple

    Manejo de errores: más allá del try/catch

    El error handling que ya tenemos en el pump del controlador cubre los fallos en el stream activo. Pero hay errores que ocurren antes del stream — cuando la API de Anthropic devuelve un 429 (rate limit) o un 500:

    // src/ai/ai.controller.ts — versión con manejo de errores completo
    import { APICallError } from 'ai';
    
    @Post('chat')
    @HttpCode(HttpStatus.OK)
    async chat(
      @Body() body: ChatRequestDto,
      @Res() res: Response,
    ): Promise<void> {
      try {
        const messages = body.messages as CoreMessage[];
        const result = this.aiService.streamChat(messages);
        const streamResponse = result.toUIMessageStreamResponse();
    
        streamResponse.headers.forEach((value, key) => {
          res.setHeader(key, value);
        });
        res.status(streamResponse.status);
    
        if (streamResponse.body) {
          const reader = streamResponse.body.getReader();
    
          const pump = async () => {
            while (true) {
              const { done, value } = await reader.read();
              if (done) { res.end(); break; }
              res.write(value);
            }
          };
    
          await pump();
        }
      } catch (error) {
        if (APICallError.isInstance(error)) {
          // Error de la API del LLM (429, 500, etc.)
          console.error('[AiController] Error API LLM:', error.message, error.statusCode);
    
          if (!res.headersSent) {
            const statusCode = error.statusCode === 429 ? 429 : 502;
            res.status(statusCode).json({
              error: error.statusCode === 429
                ? 'Demasiadas peticiones al modelo. Inténtalo en unos segundos.'
                : 'Error al conectar con el modelo de IA.',
            });
          } else {
            res.end();
          }
        } else {
          console.error('[AiController] Error inesperado:', error);
          if (!res.headersSent) {
            res.status(500).json({ error: 'Error interno del servidor.' });
          } else {
            res.end();
          }
        }
      }
    }
    

    APICallError.isInstance(error) es el type guard del AI SDK para distinguir errores de la API del LLM de errores genéricos. Útil para devolver mensajes de error específicos al cliente sin exponer detalles internos.


    Ejecutar el servidor

    # Desarrollo con hot reload
    npm run start:dev
    
    # Producción
    npm run build && npm run start:prod
    

    El servidor levanta en http://localhost:3000. Prueba el endpoint:

    curl -X POST http://localhost:3000/api/chat \
      -H "Content-Type: application/json" \
      -d '{"messages": [{"role": "user", "content": "Qué es NestJS en una frase"}]}' \
      --no-buffer
    

    Verás los chunks SSE llegar en tiempo real en la terminal. Eso confirma que el streaming funciona.


    El AiModule en producción: qué añadir después

    Lo que hemos construido es una base sólida. En un entorno de producción real, los siguientes pasos son:

    1. Autenticación. Añadir un AuthGuard de JWT al endpoint chat para que solo usuarios autenticados consuman tokens. Sin esto, cualquiera con la URL puede vaciar tu cuota.

    2. Logging estructurado. Usar @nestjs/winston o Pino para loguear cada request con userId, messageCount, y tokensUsed. El AI SDK expone usage en el stream — puedes capturarlo en el onFinish callback de streamText.

    3. Persistencia del historial. El backend actual es stateless — el historial viene del cliente en cada request. En producción con usuarios autenticados, guarda el historial en base de datos y envía solo el conversationId desde el frontend. El servidor reconstruye el historial antes de llamar al modelo.

    4. Selección de modelo por request. Si tu app da a los usuarios la opción de elegir entre Claude Sonnet y Claude Haiku (más barato), añade un campo model al DTO y pásalo al servicio. La abstracción del AI SDK hace que el cambio sea trivial.

    Si quieres profundizar en este tipo de decisiones de arquitectura — cómo estructurar un producto completo con IA desde la idea hasta producción — en el curso Construye con IA: de la idea al producto con Claude Code lo vemos con proyectos reales, no con demos de laboratorio.


    FAQ

    ¿Puedo usar este módulo con Fastify en lugar de Express?

    Sí, pero el pump manual del controlador cambia. Fastify usa Reply en lugar de Response de Express, y el método para escribir chunks es reply.raw.write(). El @Res() res: Response del controlador funcionará si configuras passThrough: true en el decorador: @Res({ passThrough: false }). La lógica del pump en sí no cambia — solo los métodos de la respuesta.

    ¿El rate limiting con ThrottlerGuard funciona bien detrás de un proxy o load balancer?

    Por defecto, ThrottlerGuard usa la IP del request. Si tu app está detrás de un proxy (Nginx, Cloudflare, etc.), la IP será siempre la del proxy. Configura ThrottlerModule con throttlers y usa ThrottlerGuard extendido que lea X-Forwarded-For. Alternativamente, delega el rate limiting al proxy — Nginx tiene limit_req_zone para esto.

    ¿Cómo evito que el stream consuma tokens si el cliente desconecta?

    streamText del AI SDK no cancela automáticamente la petición a Anthropic cuando el cliente cierra la conexión HTTP. Para implementar cancelación, pasa un AbortSignal a streamText:

    streamChat(messages: CoreMessage[], signal?: AbortSignal) {
      return streamText({
        model: this.anthropic('claude-sonnet-4-6'),
        messages,
        abortSignal: signal,
      });
    }
    

    En el controlador, escucha el evento close de la respuesta y llama a abortController.abort(). Esto cancela la llamada a la API antes de que el modelo termine de generar.

    ¿Puedo usar @ai-sdk/openai o @ai-sdk/google en lugar de Anthropic?

    Sí. Cambia createAnthropic por createOpenAI o createGoogleGenerativeAI en AiService y actualiza el nombre del modelo. El resto del módulo — controlador, DTO, rate limiting, manejo de errores — no cambia. Esa es exactamente la ventaja de usar el AI SDK como capa de abstracción: cambias de proveedor en un sitio.

    ¿CoreMessage[] es compatible con el formato de mensajes que manda el componente Angular del post anterior?

    CoreMessage del AI SDK acepta objetos con role ('user', 'assistant', 'system') y content (string). El ChatMessage del componente Angular del post anterior tiene exactamente esa forma. El cast body.messages as CoreMessage[] funciona directamente — no necesitas transformar nada.


    Cierre

    Un backend de streaming de IA no es complicado. Lo que sí es complicado es hacerlo bien desde el principio: que valide los inputs, que no queme tokens cuando el cliente desconecta, que no se caiga cuando Anthropic devuelve un 429, que tenga un límite razonable de peticiones por IP.

    NestJS más el Vercel AI SDK resuelven ese conjunto de problemas con una arquitectura que ya conoces si llevas tiempo en el ecosistema TypeScript. No hay magia — hay módulos, servicios, inyección de dependencias, y un stream que fluye limpio de principio a fin.

    El AiModule que has construido hoy es reutilizable. Impórtalo en cualquier NestJS existente, ajusta el system prompt y el modelo, y tienes un endpoint de IA en producción en menos de una hora.

    Si quieres llevarlo más lejos — tool calls, agentes con memoria, pipelines de documentos — en Dominicode Labs tenemos los proyectos completos con los patrones que usamos en producción, incluyendo ejemplos de NestJS con AI SDK con autenticación, persistencia y cancelación de streams.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Next.js 16.3 Instant Navigations: cero esperas al navegar

    Next.js 16.3 Instant Navigations: cero esperas al navegar

    Llevaba semanas con una queja recurrente de un cliente. Su app en Next.js con App Router se sentía lenta. No el server, no la base de datos — las navegaciones. Hacías clic en un enlace y durante un segundo entero no pasaba nada. Literalmente nada. Next.js 16.3 Instant Navigations es la respuesta directa a ese problema.

    En un modelo server-driven, cada navegación implica un roundtrip de red. El cliente espera, el servidor procesa, responde, aparece la página. En una SPA ese segundo no existe — el cliente muestra una shell inmediata mientras los datos llegan. Next.js había apostado por el servidor, pero el coste en percepción de velocidad era real.

    Nota de versión: Next.js 16.3 está actualmente en preview (instalable con npm install next@preview). Las APIs que describo aquí son las publicadas el 25 de junio de 2026 en el blog oficial de Next.js. Pueden cambiar antes del release estable.


    El problema que 16.3 viene a resolver

    En Next.js clásico con Server Components, el flujo de navegación era este:

    1. El usuario hace clic en un enlace.
    2. El navegador no hace nada visible.
    3. El servidor procesa la ruta, genera el HTML, responde.
    4. La página aparece.

    Para apps orientadas a contenido — un blog, un periódico — esto funciona. Para dashboards, herramientas internas, apps tipo SaaS, ese segundo de nada destruye la experiencia.

    Las SPAs resuelven esto de otra forma: descargan el código de cada ruta de antemano y muestran una shell inmediata mientras los datos llegan. Sensación instantánea. Next.js tenía el prefetching, pero lo hacía a nivel de link individual, lo que generaba decenas de peticiones al servidor cada vez que el usuario hacía scroll por una lista de enlaces.

    16.3 cambia los dos flancos del problema.


    Cómo funciona Instant Navigations

    Paso 1: habilitar Cache Components

    Todo empieza con un flag en next.config.ts:

    // next.config.ts
    import type { NextConfig } from 'next';
    
    const nextConfig: NextConfig = {
      cacheComponents: true,
    };
    
    export default nextConfig;
    

    Este flag activa el modelo de Cache Components — el nuevo paradigma de Next.js donde el caching es explícito con 'use cache' en lugar de implícito y confuso como en versiones anteriores. Con él habilitado, Next.js puede generar un "shell" para cada ruta: la parte de la UI que puede renderizarse sin esperar al servidor.

    Paso 2: elegir el modo de cada ruta — Stream, Cache o Block

    Con Cache Components activo, cuando una ruta hace await a datos del servidor, Next.js en desarrollo te muestra un panel llamado Instant Insights. Este panel detecta qué rutas están bloqueando la navegación y te da tres opciones:

    Stream con <Suspense>

    La ruta muestra inmediatamente una shell con estados de carga, y los datos se van incluyendo por streaming conforme llegan:

    // app/products/[id]/page.tsx
    import { Suspense } from 'react';
    import { ProductDetail } from './product-detail';
    import { ProductSkeleton } from './product-skeleton';
    
    export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
      const { id } = await params;
      return (
        <div>
          <h1>Producto</h1>
          <Suspense fallback={<ProductSkeleton />}>
            <ProductDetail id={id} />
          </Suspense>
        </div>
      );
    }
    

    La navegación es inmediata. El usuario ve la shell con el skeleton. Los datos llegan después. Sensación de SPA.

    Cache con 'use cache'

    Si la ruta depende de datos que se pueden cachear, marcas la función con 'use cache' y Next.js sirve el resultado cacheado de forma instantánea en navegaciones posteriores:

    // app/dashboard/analytics/page.tsx
    import { unstable_cacheLife as cacheLife } from 'next/cache';
    import { Suspense } from 'react';
    
    async function AnalyticsSection() {
      'use cache';
      cacheLife('minutes'); // TTL de caché explícito
      const data = await fetchAnalytics();
      return <Chart data={data} />;
    }
    
    export default function AnalyticsPage() {
      return (
        <Suspense fallback={<AnalyticsSkeleton />}>
          <AnalyticsSection />
        </Suspense>
      );
    }
    

    El usuario ve el contenido cacheado de forma inmediata. Si el cache está fresco, la experiencia es idéntica a una SPA.

    Block: cuando quieres que la navegación espere al servidor

    Hay casos donde no quieres mostrar una shell. Un blog no debería mostrar un spinner donde va el artículo — o muestras el artículo o no navegas. Para esos casos, exportas instant = false:

    // app/blog/[slug]/page.tsx
    export const instant = false; // Esta ruta bloquea hasta tener respuesta del servidor
    
    export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
      const { slug } = await params;
      const post = await getPost(slug);
      return <Article post={post} />;
    }
    

    Next.js deja de reportar esta ruta como problema de rendimiento. Has decidido conscientemente que prefieres la espera a mostrar una UI incompleta. La diferencia es que ahora es una decisión explícita, no un comportamiento por defecto que no entiendes.


    Partial Prefetching: prefetchear smarter, no harder

    El segundo gran cambio es cómo Next.js hace prefetching.

    En 16.2, si tenías una lista de veinte enlaces a /chat/[id], Next.js enviaba veinte peticiones de prefetch al servidor — una por link visible en el viewport. Ineficiente y costoso.

    En 16.3, con Partial Prefetching habilitado, Next.js prefetchea un shell por ruta, no por link. Veinte links a /chat/[id] generan exactamente una petición: la del shell de /chat/[id]. Ese shell se cachea en el cliente durante toda la sesión.

    Para habilitarlo:

    // next.config.ts
    import type { NextConfig } from 'next';
    
    const nextConfig: NextConfig = {
      cacheComponents: true,
      partialPrefetching: true,
    };
    
    export default nextConfig;
    

    Prefetching por link cuando necesitas más

    El Partial Prefetching es conservador por diseño — solo prefetchea el shell. Si quieres que un link concreto prefetchee también contenido específico, añades prefetch={true} al componente <Link>:

    // Una lista donde quieres que el header del chat se vea instantáneamente
    export function ChatList({ chats }: { chats: Chat[] }) {
      return (
        <ul>
          {chats.map(chat => (
            <li key={chat.id}>
              {/* Prefetch completo para este link */}
              <Link href={`/chat/${chat.id}`} prefetch={true}>
                {chat.title}
              </Link>
            </li>
          ))}
        </ul>
      );
    }
    

    Y si quieres que el prefetch incluya contenido dinámico de request-time (no solo build-time), lo permites explícitamente en la ruta:

    // app/chat/[id]/page.tsx
    export const prefetch = 'allow-runtime';
    

    La ventaja respecto al comportamiento anterior: ya no es todo o nada. Tienes granularidad real.


    Navigation Inspector: ve el shell antes de que el usuario llegue

    El Navigation Inspector es una herramienta de las Next.js DevTools que pausa cualquier navegación en el momento exacto del shell — antes de que lleguen los datos del servidor — mostrando visualmente qué partes de la ruta son instantáneas y cuáles requieren una petición de red.

    En la práctica: haces clic en un enlace, el inspector lo detiene en el shell, ves el mapa completo de tu ruta. Cuando haces clic en "Resume", la navegación completa. Especialmente útil para identificar componentes que bloquean la navegación porque hacen await sin <Suspense> ni 'use cache'.


    Testing: el helper instant() para Playwright

    Para que las mejoras de rendimiento no retrocedan con refactorizaciones futuras, Next.js 16.3 incluye un test helper para Playwright:

    // tests/navigation.spec.ts
    import { expect, test } from '@playwright/test';
    import { instant } from '@next/playwright';
    
    test('el header del producto aparece sin esperar al servidor', async ({ page }) => {
      await page.goto('/products/shoes');
    
      // Todo lo que esté dentro de este bloque debe ser visible SIN red
      await instant(page, async () => {
        await page.click('a[href="/products/hats"]');
        await expect(page.locator('h1')).toContainText('Baseball Cap');
        await expect(page.getByText('Checking inventory...')).toBeVisible();
      });
    
      // Esto sí puede esperar al servidor
      await expect(page.getByText('12 in stock')).toBeVisible();
    });
    

    instant() es el equivalente a un test de performance integrado en tu suite de e2e. Si un refactor convierte una ruta Stream en una ruta bloqueante, el test falla. Sin sorpresas en producción.

    Si ya tienes tests de Angular y quieres aplicar la misma mentalidad a tus proyectos — testear comportamiento, no implementación — el curso de Testing en Angular te da esa base de forma sólida con Jest y Testing Library.


    Comparativa: antes vs. después

    Aspecto Next.js 16.2 Next.js 16.3
    Comportamiento por defecto Bloquea hasta respuesta del servidor Stream o Cache para navegación inmediata
    Prefetching 1 petición por link en viewport 1 shell por ruta, reutilizado entre links
    Control por ruta No hay export const instant = false para rutas bloqueantes
    Herramienta de diagnóstico Ninguna Instant Insights + Navigation Inspector
    Testing de regresiones Manual instant() helper para Playwright
    Configuración Implícita y confusa Explícita con cacheComponents y partialPrefetching

    Cómo empezar hoy mismo

    Para probar Instant Navigations en un proyecto existente:

    1. Instala el preview:

      npm install next@preview
      
    2. Habilita los flags en next.config.ts:

      const nextConfig: NextConfig = {
        cacheComponents: true,
        partialPrefetching: true,
      };
      
    3. Arranca el servidor de desarrollo. Verás el panel Instant Insights con las rutas que están bloqueando la navegación.

    4. Identifica, decide y verifica. Para cada ruta bloqueante, elige Stream, Cache o Block. El Navigation Inspector confirma que el shell funciona antes de ir a producción.

    El equipo de Vercel lo validó en v0 — su propia app — antes del release. Los tiempos de navegación bajaron significativamente en las rutas que adoptaron el nuevo modelo.

    Si quieres ver cómo se integra este tipo de arquitectura con IA y streaming en tiempo real, en Dominicode Labs estamos construyendo proyectos que combinan Next.js con streaming de LLMs — exactamente el tipo de apps donde Instant Navigations marca la diferencia más visible. Para entender la Claude API con TypeScript antes de integrarla, este crash course es el punto de partida.


    Casos de uso donde esto cambia más

    Dashboards con datos en tiempo real. Cada cambio de sección era un segundo de espera. Con Stream + Suspense, el layout del dashboard aparece inmediatamente y los datos llegan después.

    Apps de chat o mensajería. Con Partial Prefetching, navegar entre conversaciones — aunque sean decenas — genera una sola petición de prefetch por ruta, no una por cada enlace visible.

    E-commerce. Las páginas de producto pueden mostrar la estructura (imagen placeholder, nombre, botón "Añadir al carrito") de forma instantánea mientras el inventario y el precio se cargan.

    Herramientas internas con muchas secciones. El menú lateral con 30 links ya no genera 30 peticiones de prefetch al cargar la página.

    Si trabajas con Astro para partes estáticas de tu sitio y Next.js para las dinámicas, el análisis de Astro v7 te ayuda a decidir qué encaja en cada capa.


    FAQ

    ¿Instant Navigations funciona con el Pages Router o solo con App Router?

    Solo con App Router. Cache Components y el modelo de shells requieren Server Components, que no existen en Pages Router. Si aún tienes un proyecto en Pages Router, esta es una razón más para evaluar la migración.

    ¿cacheComponents: true cambia el comportamiento de caching de mis datos?

    Sí, de forma intencional. El nuevo modelo hace el caching explícito: nada se cachea por defecto a menos que uses 'use cache'. Si venías de fetch con opciones implícitas de cache, tendrás que revisar tu estrategia de datos. Es un cambio de paradigma, no solo un flag de navegación.

    ¿El Partial Prefetching incrementa el coste de servidor?

    Al contrario. En 16.2, con veinte links en pantalla tenías veinte peticiones de prefetch. En 16.3, con Partial Prefetching, tienes una petición por ruta distinta. En un escenario real con listas de items que apuntan al mismo route pattern, la reducción de peticiones puede ser del 90%.

    ¿Puedo usar <Link prefetch={true}> para todo y obtener el comportamiento anterior?

    Técnicamente sí, pero estarías ignorando el punto. El comportamiento anterior era ineficiente. <Link prefetch={true}> existe para casos específicos donde necesitas prefetchear más que el shell en un link concreto — no como reemplazo global del viejo modelo.

    ¿Cuándo sale el release estable de Next.js 16.3?

    No hay fecha oficial confirmada. El equipo indica que están resolviendo issues conocidos (algunos casos con Safari en Instant Insights, y rutas bloqueantes que no se reportan correctamente con Partial Prefetching activo). La recomendación es probar el preview en proyectos de desarrollo, no en producción.

    ¿Puedo escribir tests con instant() antes del release estable?

    Sí. El paquete @next/playwright ya incluye el helper y es seguro usarlo en tu suite de e2e. Si el release estable cambia el comportamiento, los tests te lo dirán antes de que llegue a producción.


    Next.js 16.3 no reinventa el framework. Lo que hace es cerrar la brecha más molesta que tenían los Server Components: la sensación de lentitud al navegar. Stream, Cache y Block son tres palabras, pero detrás hay un modelo de pensamiento claro sobre qué parte de tu UI puede ser instantánea y cuál no.

    La clave no está en activar los flags y esperar magia. Está en recorrer tus rutas con el Navigation Inspector, entender qué está bloqueando, y tomar la decisión correcta para cada una.

    Si tu próximo proyecto combina Next.js con agentes de IA o herramientas de Claude Code, en el curso Construye con IA vemos exactamente cómo estructurar apps que necesitan streaming, caché inteligente y navegación fluida desde el primer día.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Astro v7: novedades clave y cómo crear tu landing desde cero

    Astro v7: novedades clave y cómo crear tu landing desde cero

    Un developer me escribió hace unas semanas. Tenía una landing page para un producto que estaba a punto de lanzar. La había montado con Next.js porque “era lo que conocía”. El tiempo de build era de 4 minutos. El bundle pesaba más de lo que debería. Y lo único que necesitaba era una página estática con una sección hero, tres features y un formulario de contacto.

    Next.js para eso es como usar un martillo neumático para clavar un cuadro.

    La respuesta que le di fue una sola palabra: Astro. Y ahora, con Astro v7, esa respuesta es más sólida que nunca.


    Qué es Astro, por si llevas tiempo mirando para otro lado

    Astro es un framework de generación de sitios estáticos que tiene una idea central brillante: envía cero JavaScript al cliente por defecto. Solo envía HTML y CSS. Si necesitas interactividad en algún componente concreto, la añades con lo que quieras: React, Vue, Svelte, Lit — Astro lo llama “islands”.

    Para landings, blogs, documentación y cualquier sitio orientado al contenido, no hay nada más rápido ni más sencillo de mantener.

    Astro v7 lleva esa idea más lejos que nunca. Y los números lo avalan.


    Las novedades reales de Astro v7

    Astro v7 es la séptima versión mayor del framework, lanzada en 2026, que introduce un compilador reescrito en Rust, Vite 8 con Rolldown y Route Caching estable como cambios principales. Es la versión con el mayor salto de rendimiento desde el lanzamiento del framework.

    El compilador se reescribió en Rust — y se nota

    La parte más importante de esta versión no es una nueva API. Es que el compilador de .astro pasó de estar escrito en Go a estar escrito en Rust.

    El resultado: builds entre un 15% y un 61% más rápidos en benchmarks reales. El sitio de documentación oficial de Astro pasó de tardar 114 segundos en construirse a 73. El propio astro.build bajó de 62 segundos a 24 (datos oficiales del blog de Astro).

    Eso es el tipo de mejora que no se consigue ajustando configuraciones. Es un cambio de arquitectura.

    El tradeoff: el compilador Rust es más estricto con HTML inválido. Antes, si dejabas una etiqueta sin cerrar, Astro la corregía silenciosamente. Ahora lanza un error. Lo cual, seamos honestos, es el comportamiento correcto.

    Vite 8 con Rolldown — el bundler en Rust también

    Astro v7 actualiza a Vite 8, que trae Rolldown: un bundler escrito en Rust que reemplaza tanto a esbuild como a Rollup con una sola herramienta unificada. En benchmarks, es 10-30 veces más rápido que Rollup.

    La compatibilidad con la API de plugins de Rollup y Vite se mantiene. Si tienes plugins existentes, seguirán funcionando.

    Nuevo procesador Markdown: Sätteri

    El pipeline de Markdown ha cambiado por completo. El procesador anterior basado en unified, remark y rehype ha sido reemplazado por Sätteri, un procesador escrito también en Rust.

    Sätteri incluye de serie: GitHub Flavored Markdown, tipografía inteligente, IDs de encabezados, directivas, matemáticas y frontmatter. Sin configuración adicional.

    Si tenías plugins personalizados de remark o rehype, puedes volver al pipeline anterior instalando @astrojs/markdown-remark y configurándolo explícitamente. No pierdes nada — solo dejas de tenerlo por defecto.

    Route Caching ahora es estable

    El sistema de caché de rutas, que estaba en experimental, ya es parte de la API estable. Puedes controlar el caché de cada respuesta con una API agnóstica a la plataforma:

    // En cualquier página .astro o endpoint
    Astro.cache.set({
      maxAge: 120,     // 2 minutos en caché de cliente/servidor
      swr: 60,         // 1 minuto de revalidación en background
      tags: ['products']
    });

    Y cuando necesitas invalidar:

    await cache.invalidate({ tags: ['products'] });

    Para Netlify, Vercel y Cloudflare hay CDN cache providers experimentales que traducen estas directivas a la capa edge de cada plataforma.

    Advanced Routing — control total del pipeline

    Astro v7 introduce un archivo src/fetch.ts que te da acceso completo al pipeline de solicitudes, antes de que Astro las procese. Compatible con Hono para middleware:

    // src/fetch.ts
    import { astro } from 'astro/fetch';
    
    

    export default { fetch(request: Request) { const url = new URL(request.url);

    // Intercepta rutas /api antes de que lleguen a Astro if (url.pathname.startsWith('/api')) { return fetch('https://mi-backend.com' + url.pathname, request); }

    return astro(request); } }

    Si ya tienes un src/fetch.ts propio en tu proyecto, Astro v7 lo detectará y te pedirá que lo renombres o que desactives esta feature.

    Modo para agentes IA

    Esto es interesante si construyes herramientas con IA o trabajas con Claude Code, Cursor u otros coding agents. Astro v7 detecta automáticamente cuando está corriendo dentro de un agente y activa dos comportamientos:

    1. Inicia el servidor de desarrollo en modo background con astro dev --background
    2. Cambia los logs a formato JSON estructurado para que el agente los pueda parsear
    # Arranque idempotente — no lanza otro servidor si ya hay uno corriendo
    astro dev --background
    
    

    # Estado del servidor astro dev status

    # Logs en JSON astro dev --json


    Astro v6 vs v7 — qué cambió exactamente

    Área Astro v6 Astro v7
    Compilador .astro Go Rust (15-61% más rápido)
    Bundler Vite 7 + Rollup Vite 8 + Rolldown (Rust)
    Procesador Markdown unified / remark / rehype Sätteri (Rust, GFM incluido)
    Route Caching Experimental Estable (Astro.cache.set)
    Routing avanzado No disponible src/fetch.ts estable
    Modo agente IA No disponible astro dev --background + logs JSON
    HTML inválido Corregido silenciosamente Error de compilación (más estricto)
    @astrojs/db Disponible Eliminado → migrar a node:sqlite

    Cómo crear tu landing con Astro v7 — paso a paso

    Paso 1 — Instalación

    npm create astro@latest mi-landing

    El wizard te preguntará si quieres una plantilla vacía, con blog, o con un starter. Para una landing, elige “Empty” o “A basic, minimal starter”.

    cd mi-landing
    npm run dev

    Tu servidor de desarrollo estará en http://localhost:4321.

    Paso 2 — Estructura del proyecto

    mi-landing/
    ├── public/
    │   └── favicon.svg
    ├── src/
    │   ├── components/
    │   │   ├── Header.astro
    │   │   ├── Hero.astro
    │   │   ├── Features.astro
    │   │   └── Footer.astro
    │   ├── layouts/
    │   │   └── BaseLayout.astro
    │   └── pages/
    │       └── index.astro
    ├── astro.config.mjs
    └── package.json

    Paso 3 — El layout base

    ---
    // src/layouts/BaseLayout.astro
    interface Props {
      title: string;
      description: string;
    }
    
    

    const { title, description } = Astro.props;


    <!doctype html> <html lang="es"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="description" content={description} /> <title>{title}</title> </head> <body> <slot /> </body> </html>

    Paso 4 — Los componentes de la landing

    ---
    // src/components/Hero.astro
    interface Props {
      headline: string;
      subheadline: string;
      ctaText: string;
      ctaHref: string;
    }
    
    

    const { headline, subheadline, ctaText, ctaHref } = Astro.props;


    <section class="hero"> <h1>{headline}</h1> <p>{subheadline}</p> <a href={ctaHref} class="cta-btn">{ctaText}</a> </section>

    <style> .hero { display: flex; flex-direction: column; align-items: center; padding: 4rem 1rem; text-align: center; gap: 1.5rem; }

    h1 { font-size: clamp(2rem, 5vw, 3.5rem); font-weight: 800; line-height: 1.1; }

    .cta-btn { display: inline-block; padding: 0.9rem 2rem; background: #7c3aed; color: white; text-decoration: none; border-radius: 0.5rem; font-weight: 600; transition: opacity 0.2s; }

    .cta-btn:hover { opacity: 0.85; } </style>

    ---
    // src/components/Features.astro
    const features = [
      {
        icon: "⚡",
        title: "Velocidad real",
        description: "HTML puro en el cliente. Sin JavaScript innecesario."
      },
      {
        icon: "🧩",
        title: "Componentes modulares",
        description: "Divide la UI en piezas reutilizables con props tipadas."
      },
      {
        icon: "🚀",
        title: "Deploy en segundos",
        description: "Estático por defecto. Netlify, Vercel o Cloudflare Pages."
      }
    ];
    

    <section class="features"> <h2>Por qué esto funciona</h2> <ul class="features-grid"> {features.map((feature) => ( <li> <span class="icon">{feature.icon}</span> <h3>{feature.title}</h3> <p>{feature.description}</p> </li> ))} </ul> </section>

    <style> .features { padding: 4rem 1rem; max-width: 900px; margin: 0 auto; }

    h2 { text-align: center; font-size: 2rem; margin-bottom: 2.5rem; }

    .features-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 2rem; list-style: none; padding: 0; }

    .icon { font-size: 2rem; display: block; margin-bottom: 0.75rem; }

    h3 { font-size: 1.1rem; margin-bottom: 0.5rem; } </style>

    Paso 5 — La página principal que lo une todo

    ---
    // src/pages/index.astro
    import BaseLayout from '../layouts/BaseLayout.astro';
    import Hero from '../components/Hero.astro';
    import Features from '../components/Features.astro';
    

    <BaseLayout title="Mi Producto — La forma más rápida de hacer X" description="Descripción de 160 caracteres para el SEO." > <Hero headline="Resuelve X sin Y" subheadline="La frase que convierte la curiosidad en intención de compra." ctaText="Empieza gratis" ctaHref="#registro" /> <Features /> </BaseLayout>

    Paso 6 — Build y deploy

    # Build de producción
    npm run build
    
    

    # Preview local del build npm run preview

    El output es estático por defecto: la carpeta dist/ contiene HTML, CSS y los assets optimizados. Sube esa carpeta a Netlify, Vercel o Cloudflare Pages y estás listo.


    Los breaking changes que tienes que revisar si migras desde v6

    Si ya usas Astro y estás actualizando, el comando oficial es:

    npx @astrojs/upgrade

    Pero antes de ejecutarlo, ten en cuenta estos cuatro puntos:

    1. Etiquetas HTML sin cerrar ahora son errores. El compilador Rust no las corrige. Revisa tus componentes .astro buscando

    ,

  • o cualquier etiqueta no-void sin su cierre correspondiente.

    2. @astrojs/db ha sido eliminado. Si lo usabas, migra a node:sqlite (disponible desde Node.js 22.5.0), Drizzle ORM, Turso o cualquier alternativa SQL moderna.

    3. Los eventos de astro:transitions cambiaron. TRANSITION_BEFORE_PREPARATION y similares desaparecen. Sus equivalentes son strings directos como 'astro:after-swap'.

    4. Si tenías features en experimental, sácalas de ahí. queuedRendering, advancedRouting, cache y logger ya son estables. Muévelos al nivel raíz de astro.config.mjs.


    Por qué Astro v7 importa ahora mismo

    Astro es la respuesta correcta para landings y sites de documentación. No porque React sea malo, sino porque la herramienta correcta para contenido estático no es un SPA. Si estás construyendo productos con IA y quieres entender cómo encaja el frontend en ese stack, el post sobre el stack IA agéntica en 2026 te da el mapa completo de herramientas.

    El patrón que más se repite en los proyectos con IA que veo últimamente: alguien genera una landing con un agente y lo hace con el stack que “siempre ha usado”. El resultado: un bundle de React para servir contenido estático.

    Astro es la respuesta correcta para ese caso de uso. No porque React sea malo, sino porque la herramienta correcta para una landing o un site de documentación no es un SPA.

    El hecho de que Astro v7 ahora detecte agentes IA automáticamente no es un detalle menor — es una señal de hacia dónde va el ecosistema. Si usas Claude Code o Cursor para generar código, el post sobre cómo funciona el agentic loop explica el mecanismo detrás de esa integración.

    En el curso de Construye con IA trabajamos exactamente este tipo de decisiones: cuándo elegir Astro, cuándo Next.js tiene sentido, y cómo pasar de una idea a un producto sin arrastrar deuda técnica desde el primer día.

    Con el enfoque de Spec-Driven Development, diseñar la arquitectura de este tipo de landing antes de escribir la primera línea de código es lo que separa una landing que escala de una que se convierte en un problema de mantenimiento en seis meses.


    FAQ

    ¿Astro v7 es compatible con React, Vue o Svelte?

    Sí. Astro sigue siendo agnóstico al framework de UI. Puedes usar componentes de React, Vue, Svelte, Solid o cualquier otro framework compatible mediante el sistema de integraciones. Lo que cambia es que Astro no envía el runtime de esos frameworks al cliente a menos que marques explícitamente un componente con una directiva client:*.

    ¿Necesito migrar a Sätteri si tengo plugins de remark personalizados?

    No es obligatorio. Puedes instalar @astrojs/markdown-remark y configurar markdown: { processor: unified() } en tu astro.config.mjs para mantener el pipeline anterior. Sätteri es el nuevo default, pero el viejo pipeline sigue disponible.

    ¿Puedo usar Astro v7 para sitios con contenido dinámico, no solo estático?

    Sí. Astro soporta SSR (Server-Side Rendering) con adaptadores para Node.js, Netlify, Vercel y Cloudflare Workers. Con el nuevo Advanced Routing y src/fetch.ts tienes control total sobre el pipeline de solicitudes. Para contenido que cambia con frecuencia, el nuevo sistema de Route Caching con invalidación por tags es especialmente útil.

    ¿Cuánto cuesta migrar un proyecto de Astro v5 o v6 a v7?

    Depende del proyecto, pero el comando npx @astrojs/upgrade automatiza la mayor parte. Los cambios manuales más comunes son: cerrar etiquetas HTML que el compilador anterior perdonaba, eliminar @astrojs/db si lo usabas, y sacar las features que estaban en experimental al nivel raíz de la configuración. En un proyecto mediano, una tarde es suficiente.

    ¿Astro v7 es una buena opción para documentación técnica?

    Es probablemente la mejor opción disponible. El procesador Sätteri maneja GFM, directivas y matemáticas de serie. El sistema de content collections te permite estructurar MDX como si fuera una base de datos. Y los builds son ahora significativamente más rápidos, lo que importa cuando tienes cientos de páginas de documentación. No en vano, el propio sitio de documentación de Astro corre sobre Astro.


    Si quieres ver cómo integramos herramientas como Astro en un flujo de trabajo completo con IA — desde el spec hasta el deploy — en Dominicode Labs tenemos proyectos activos donde exploramos exactamente ese proceso.

    Y si prefieres empezar con vídeo, en el canal de YouTube de Dominicode hay contenido regular sobre desarrollo con IA, Angular, TypeScript y herramientas de producción.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Nuevo Vite 8.1

    Nuevo Vite 8.1

    El lunes arrancas la semana, abres el proyecto, y ves que hay una nueva versión de Vite disponible. La actualizas casi en automático — llevas años confiando en que las minor no rompen nada.

    Esta vez, una advertencia en la consola: está deprecado. Buscas en la documentación. El nombre cambió. Nada grave, pero te detiene cinco minutos.

    Eso es exactamente lo que hace Vite 8.1: llega sin hacer ruido, resuelve cosas que probablemente ya te estaban molestando sin que lo supieras, y mete una feature experimental que puede cambiar cómo experimentas el desarrollo en proyectos grandes. Si todavía no la has revisado, este post te ahorra el tiempo de buscarla tú.

    se publicó el 23 de junio de 2026 y, según las propias métricas del proyecto, ya hay 41,6 millones de descargas semanales — casi las mismas que acumuló toda la era de Vite 7.

    Vite 8.1 es la versión minor del bundler de frontend que introduce bundled dev mode experimental, soporte WASM ESM nativo, chunk import maps y mejoras en LightningCSS — todo sobre la base de Rolldown, el motor Rust que reemplazó a esbuild y Rollup en Vite 8.0.


    El dev server de Vite siempre ha funcionado en modo _unbundled_: sirve cada módulo como un fichero independiente aprovechando ESM nativo del navegador. Es lo que lo hace rápido en proyectos medianos. El problema viene cuando el proyecto crece.

    En una app con 10.000 componentes React, el navegador tiene que resolver 10.000 requests en cadena. El HMR se mantiene instantáneo, pero la carga inicial y los reloads completos se vuelven lentos.

    junta los módulos antes de servirlos — igual que haría un build de producción, pero con la ventaja de que el HMR sigue siendo incremental y preciso. Los números que publica el equipo de Vite:

    • ~15x de arranque más rápido para apps grandes
    • ~10x de reloads completos más rápidos
    • 10x menos requests de red

    El equipo de Linear (la herramienta de gestión de proyectos) lo probó en producción: cold start 3x más rápido, reloads 40% más rápidos.

    Para activarlo, tienes dos opciones:

    # Desde la CLI
    vite --experimental-bundle
    // vite.config.ts
    export default defineConfig({
      experimental: {
        bundledDev: true,
      },
    })

    Es experimental. No lo actives en CI ni en producción todavía — el equipo todavía lo está refinando. Pero en desarrollo local, sobre todo si tu proyecto tiene cientos de rutas o módulos, pruébalo esta semana.


    Hasta ahora, importar un módulo WebAssembly en Vite requería configuración extra o un plugin. En Vite 8.1 ya es nativo con la integración de :

    import { add } from './add.wasm'
    
    

    console.log(add(1, 2)) // 3

    Sin configuración adicional. Sin plugins. El módulo expone sus funciones directamente como named exports de ES Module.

    Esto es relevante si estás construyendo herramientas de procesamiento de datos, encoders, parsers, o cualquier lógica computacionalmente intensiva que tenga sentido compilar desde Rust o C. El flujo de trabajo Rust → → Vite ya no tiene fricción.


    El problema de los hashes en cascada es conocido: cambias una línea en un componente, el hash de ese chunk cambia, y ese cambio se propaga al chunk que lo importa, que cambia su hash también, y así en cadena. El resultado: el navegador invalida más caché de la necesaria.

    Vite 8.1 introduce soporte para como feature experimental. En lugar de que los hashes de los módulos dependientes cambien cuando cambia un imported chunk, el import map actúa como capa de indirección. El chunk padre sigue apuntando al mismo nombre lógico; el import map resuelve el nombre al hash real.

    // vite.config.ts
    export default defineConfig({
      experimental: {
        bundledDev: true,
        // chunkImportMap se activa automáticamente con bundledDev
      },
    })

    No requiere configuración adicional en . Limitación actual: no es compatible con . Si lo usas, espera a que lo resuelvan antes de activarlo.


    Vite lleva varios releases preparando el terreno para que LightningCSS sustituya a PostCSS como procesador CSS por defecto. En 8.1 llegan dos adiciones concretas:

    1. dentro de archivos CSS cuando usas LightningCSS
    2. mediante plugins — necesario para que el HMR funcione correctamente cuando un plugin modifica ficheros que no están directamente en el grafo de dependencias

    Para activar LightningCSS:

    // vite.config.ts
    export default defineConfig({
      css: {
        transformer: 'lightningcss',
      },
    })

    Si ya estás en , estas dos adiciones te llegan sin hacer nada. Si sigues con PostCSS, no hay prisa — pero la dirección del proyecto es clara.


    tiene un bug silencioso en el que la opción funcionaba en la resolución inicial de módulos pero no en el matching del HMR. Cambiabas un fichero, y Vite no lo detectaba si el nombre diferenciaba mayúsculas/minúsculas de forma inesperada.

    En 8.1 ya está corregido:

    const modules = import.meta.glob('./dir/module*.js', {
      caseSensitive: false,
    })

    Ahora tanto la resolución inicial como el HMR respetan la misma sensibilidad a mayúsculas. Si tenías workarounds para esto, puedes eliminarlos.


    Vite detecta assets en el HTML buscando atributos conocidos: , , , etc. Si tienes elementos o atributos custom que referencian assets, Vite los ignoraba.

    Ahora puedes configurarlo explícitamente:

    // vite.config.ts
    export default defineConfig({
      html: {
        additionalAssetSources: [
          { tag: 'my-image', attribute: 'data-src' },
          { tag: 'x-video', attribute: 'poster-url' },
        ],
      },
    })

    Útil si trabajas con Web Components o frameworks que usan atributos no estándar para referenciar recursos.


    Vite ahora amplía la lista de ficheros denegados por defecto en . Ficheros comunes que no deberían exponerse en el dev server quedan bloqueados sin que tengas que configurarlo manualmente.

    Si tienes una configuración explícita de , revísala — puede haber solapamientos. Si confías en el comportamiento por defecto, simplemente tienes una superficie de ataque más pequeña sin hacer nada.


    Este es el cambio que genera la advertencia de deprecación que mencioné al principio.

    La opción tenía un nombre ambiguo — HMR es el protocolo, pero la configuración afectaba al WebSocket server completo, que también se usa para comunicaciones que no son HMR. El nuevo nombre refleja mejor qué estás configurando.

    // Antes (deprecado en 8.1)
    export default defineConfig({
      server: {
        hmr: {
          port: 24678,
          host: 'localhost',
        },
      },
    })
    
    

    // Ahora export default defineConfig({ server: { ws: { port: 24678, host: 'localhost', }, }, })

    La opción antigua sigue funcionando — verás el warning pero no se rompe nada. Migra cuando puedas, no es urgente.


    Vite 8.1 integra soporte para con caché de build zero-config. Si ya estás usando Vite Task (la nueva API de tareas del ecosistema), los builds se cachean automáticamente sin configuración adicional.

    Es una adición menor para la mayoría de proyectos, pero importante si tienes monorepos donde los builds repetidos son costosos.


    La migración desde 8.0 es prácticamente sin fricción. Los pasos concretos:

    # npm
    npm install vite@^8.1 --save-dev
    
    

    # pnpm pnpm update vite@^8.1

    # yarn yarn upgrade vite@^8.1

    # bun bun update vite

    Después de actualizar, revisa estos tres puntos:

    si tienes configuración explícita del WebSocket server. La advertencia en consola te lo indica exactamente.

    si tienes reglas custom. Los nuevos defaults pueden solapar con las tuyas y crear comportamientos inesperados.

    si usas la opción directamente. Migra al sistema estándar de ficheros .

    Si vienes de Vite 7.x, revisa primero la — hay cambios más sustanciales en el salto de major, como el motor Rolldown que reemplaza a esbuild+Rollup. Precisamente, si te interesa cómo Astro v7 aprovecha Vite 8 en producción, tienes el análisis completo en .


    Si estás en Vite 7 y quieres pasar directamente a 8.1, el cambio más importante es . Vite 8.0 reemplazó esbuild (para transform) y Rollup (para bundle) por Rolldown, un bundler escrito en Rust. El resultado es hasta 30x más rápido en builds grandes, pero hay diferencias de comportamiento en edge cases.

    La documentación oficial tiene la guía completa. Lo que suele dar problemas en la práctica:

    • Plugins que asumen comportamientos específicos de Rollup en hooks de resolución
    • Configuraciones de — pasan automáticamente a pero no todo es compatible 1:1
    • Targets de CSS con PostCSS si usas configuraciones muy custom

    Mi recomendación: si tienes un proyecto en producción con Vite 7, dedica una tarde a la migración en una rama separada antes de mergearla. Si además estás evaluando qué herramientas usar en un stack moderno con IA, el post sobre el te da el mapa completo de qué merece la pena y qué ignorar. No es un upgrade de dos minutos, pero tampoco es traumático si el proyecto no tiene plugins exóticos.


    Sí. Vite 8.1 es agnóstico al framework. Los plugins oficiales (@vitejs/plugin-react, @vitejs/plugin-vue, @analogjs/vite-plugin-angular, @sveltejs/vite-plugin-svelte) ya tienen versiones compatibles con Vite 8.x. Verifica en el de cada plugin que el peerDependency incluye .

    Cuando notes que tu dev server tarda más de 3-5 segundos en arrancar o que los reloads completos son lentos. En proyectos pequeños y medianos (menos de 500 módulos), el modo unbundled sigue siendo más rápido. El beneficio de bundled dev mode es proporcional al tamaño del proyecto.

    Casi. LightningCSS cubre la mayoría de casos: prefixing, nesting nativo, variables CSS, imports. Lo que todavía puede requerir PostCSS son plugins muy específicos del ecosistema (px a rem, ciertas transformaciones custom). Si tu stack CSS es estándar, prueba a migrar — las mejoras de velocidad son notables.

    No. Esta opción solo afecta al servidor de desarrollo. En producción, Vite no levanta un WebSocket server — esa configuración es irrelevante. El cambio es puramente de nomenclatura en el dev server.

    Vite 8 requiere Node.js 18+ (igual que Vite 7). Si estás en Node 16 o inferior, tendrás que actualizarlo antes de poder usar Vite 8.x.

    Rolldown es el motor por defecto desde Vite 8.0 y se considera estable para uso en producción. Los flags experimentales son para features específicas (bundled dev mode, chunk import map) — no para Rolldown en sí. Para la mayoría de proyectos, Rolldown funciona como un drop-in replacement de Rollup.


    Vite 8.1 no es una release que te obligue a cambiar cómo trabajas. La mayor parte de las features son opt-in o correcciones de bugs que funcionan transparentemente.

    Lo que sí cambia si lo adoptas activamente:

    El puede hacer que trabajes diferente en proyectos grandes — menos tiempo esperando recargas, más tiempo en el problema real. El abre la puerta a traer lógica Rust o C al frontend sin fricción. Y el empieza a resolver uno de los problemas crónicos del caching en producción.

    Si trabajas con agentes de IA para acelerar tu desarrollo — donde cada segundo de feedback loop importa — el bundled dev mode encaja directamente en ese flujo. Es parte del tipo de optimizaciones que exploramos en el : no solo usar IA para generar código, sino construir un entorno donde iterar sea rápido de verdad.

    Y si quieres ver cómo aplico estas decisiones de toolchain en proyectos reales con código completo, pásate por — ahí está el material avanzado que no llega al blog.


  • Implementando Claude Code para la automatización de desarrollo en Angular y NestJS

    Implementando Claude Code para la automatización de desarrollo en Angular y NestJS

    Claude Code como herramienta diaria de desarrollo

    Tiempo estimado de lectura: 5 min

    • Orquestación de tareas multi-archivo y ejecución de CLI para migraciones, generación de boilerplate y correcciones automáticas.
    • Requiere contexto persistente (ej. archivo CLAUDE.md) para evitar alucinaciones y errores arquitectónicos.
    • Útil para flujos repetibles y tests automatizados; no ideal para retoques UI o tareas atómicas simples.

    Resumen rápido (lectores con prisa)

    Claude Code es un agente orientado a orquestar tareas que implican múltiples archivos y ejecución de CLI. Úsalo cuando necesites migraciones, generación de boilerplate, tests y correcciones automáticas a partir de stack traces. No es la mejor opción para escribir una sola función o pulir UI.

    Por qué usar (o no) Claude Code en tu flujo diario

    Claude Code Claude Code está pensado para tareas que van más allá del autocompletado: migraciones, generación de boilerplate, tests y correcciones automáticas tras detectar fallos en la terminal. No es mejor que Copilot para escribir una función; es más útil cuando la tarea implica múltiples archivos y ejecución de CLI.

    Ventajas reales:

    • Orquestación multi-archivo y ejecución de comandos.
    • Correcciones automáticas tras leer stack traces.
    • Generación de tests y refactors repetibles.

    Limitaciones reales:

    • Consumo alto de contexto/token en sesiones largas.
    • Riesgo de sobreescritura si la instrucción es ambigua.
    • Posible bucle de corrección ante errores complejos.

    Decisión simple: úsalo para tareas de orquestación; no para retoques visuales ni diseño fino de UI.

    Preparación: cómo darle contexto al agente

    Sin contexto, el agente alucina. La práctica que funciona es tener un archivo de contexto que el agente lea antes de actuar. Crea CLAUDE.md en la raíz:

    # CLAUDE: reglas del repo
    Stack:
    - Backend: NestJS 10 (TypeScript estricto)  https://nestjs.com/
    - Frontend: Angular 17 (standalone components, Signals)  https://angular.io/
    
    Convenciones:
    - DTOs con class-validator
    - Servicios inyectados por constructor
    - Componentes standalone, sin NgModules
    - Commits en Conventional Commits
    

    Ese archivo actúa como prompt persistente. Reduce alucinaciones arquitectónicas y mejora resultados.

    Tutorial práctico: flujo real con NestJS y Angular

    Objetivo: crear recurso Products en backend (NestJS) y consumirlo desde Angular, con tests básicos.

    1) Generar recurso en NestJS

    En la carpeta del backend:

    # instrucción al agente
    claude "Lee CLAUDE.md. Genera recurso Products en NestJS: Controller, Service, DTO CreateProductDto con class-validator. Ejecuta npm run build y corrige errores."
    

    Qué hará:

    • Ejecutará nest g res products o creará manualmente los archivos.
    • Insertará DTOs con validaciones (@IsString, @IsNumber).
    • Ejecutará npm run build; si TypeScript falla, leerá el stack trace y aplicará correcciones iterativas.

    Ejemplo mínimo de DTO que el agente debe crear:

    // create-product.dto.ts
    import { IsString, IsNumber } from 'class-validator';
    export class CreateProductDto {
      @IsString()
      name: string;
    
      @IsNumber()
      price: number;
    }
    

    2) Consumir endpoint desde Angular

    En la carpeta del frontend:

    claude "Crea ProductService usando provideHttpClient y un componente ProductFormComponent standalone. Usa Signals para estado de formulario. Ejecuta ng build y corrige tipados."
    

    Qué esperar:

    • Creación de product.service.ts con funciones que llaman al endpoint.
    • ProductFormComponent standalone con Signals para isLoading y errors.
    • ng build que verifica tipado y dependencias; el agente corrige importaciones o tipos si hay fallos.

    Fragmento esperado en Angular:

    // product.service.ts (simplificado)
    import { inject } from '@angular/core';
    import { HttpClient } from '@angular/common/http';
    export const ProductService = () => {
      const http = inject(HttpClient);
      return {
        create: (payload: any) => http.post('/api/products', payload)
      };
    };
    

    3) Generar tests automatizados

    Comando recomendado:

    claude "Genera tests Jest para products.service.ts y products.controller.ts. Ejecuta npm run test y corrige mocks hasta que la suite pase."
    

    Valor: te ahorra el 70% del trabajo repetitivo de mocks y boilerplate.

    Riesgos y contramedidas operativas

    1. Trabaja siempre en una rama aislada:
      git checkout -b feat/claude-codex
      – Nunca en main o develop.
    2. Limita la ventana de contexto:
      – Corta sesiones largas. Ejecuta tareas atómicas y revisa resultados antes de continuar.
    3. Evita permisos globales de escritura en archivos sensibles:
      – Usa .claudeignore para bloquear rutas (si la herramienta lo soporta) o un wrapper que restrinja paths.
    4. Plan para fallos en node_modules:
      – Si entra en bucle, interrumpe y ejecuta npm ci o reinstala dependencias; luego reintenta con más contexto.

    Checklist para adopción en equipo

    • [ ] CLAUDE.md con convenciones del repo.
    • [ ] Branching obligatorio para sesiones de agente.
    • [ ] Scripts de CI que validen outputs generados por el agente.
    • [ ] Monitoreo de consumo de API/tokens.
    • [ ] Política interna para revisar commits automáticos antes de merge.

    Claude Code no es una varita mágica; es una herramienta poderosa si la gobiernas. Si empiezas documentando el proyecto y limitando sus permisos, te dará horas de productividad en tareas repetitivas y orquestación. Si no, corregirás borradores y rollbacks a mano. La diferencia está en las reglas y la disciplina.

    Relacionado: visita Dominicode Labs para ver experimentos y guías sobre agentes y automatización. Esta mención encaja como continuación lógica para equipos que exploran flujos de IA aplicada y agentes.

    FAQ

    ¿Qué es Claude Code y para qué sirve?

    Claude Code es un agente diseñado para orquestar tareas que implican múltiples archivos y comandos de terminal: migraciones, generación de boilerplate, tests y correcciones automáticas tras fallos. Es especialmente útil cuando la tarea requiere ejecutar CLI y aplicar cambios iterativos.

    ¿Cuándo debería usar Claude Code en lugar de Copilot?

    Usa Claude Code cuando la tarea sea multi-archivo, requiera ejecución de comandos o correcciones a partir de stack traces. Para pequeñas funciones o autocompletado local, Copilot suele ser más eficiente.

    ¿Cómo debo preparar mi repo antes de usar el agente?

    Crea un archivo de contexto persistente (por ejemplo CLAUDE.md) con stack, convenciones y reglas del repo. Trabaja en una rama aislada y asegúrate de tener scripts de CI que validen cambios automáticos.

    ¿Qué riesgos operativos debo mitigar?

    Principales riesgos: sobreescritura de archivos, consumo excesivo de tokens en sesiones largas y bucles de corrección. Mitígalo con ramas aisladas, límites de sesión y mecanismos para restringir paths sensibles (por ejemplo .claudeignore o wrappers).

    ¿Cómo integro tests automatizados en el flujo del agente?

    Pide al agente generar tests Jest para servicios y controladores, ejecutar npm run test y corregir mocks hasta que la suite pase. Complementa con scripts de CI que validen los cambios generados antes del merge.

    ¿Qué hacer si el agente entra en bucle de correcciones?

    Interrumpe la sesión, ejecuta npm ci o reinstala dependencias, revisa el contexto y reintenta con instrucciones más atómicas y detalladas. Limitar la ventana de contexto también ayuda a evitar bucles.

  • Angular 22: Implicaciones técnicas y coste real de migrar en 2026

    Angular 22: Implicaciones técnicas y coste real de migrar en 2026

    Angular 22 vs el resto: lo que nadie te dice sobre migrar en 2026

    Tiempo estimado de lectura: 4 min

    • Modernización estructural: Zoneless por defecto, Signals y Control Flow nativo cambian la forma de detectar cambios y escribir templates.
    • Contextos de ventaja: Angular 22 aporta coherencia en equipos grandes y proyectos de larga vida útil; no es la opción por defecto para prototipado rápido.
    • Coste real de migración: incluye trabajo manual de refactor y coste de formación; planifica recursos y tiempo (ej. 3–6 meses para monorepos medianos).
    • Testing y despliegue: migrar a Jest + Angular Testing Library es parte crítica del plan; usa despliegues canarios y métricas para validar.

    Buscar Angular 22 vs el resto: lo que nadie te dice sobre migrar en 2026 no es una charla de café. Es una decisión de arquitectura con consecuencias inmediatas en costes, ritmo de desarrollo y mantenimiento. Angular 22 ya no es el framework “pesado” que recuerdas. Pero esa modernización trae costes de migración reales y decisiones estratégicas que no aparecen en la documentación oficial.

    Resumen rápido (lectores con prisa)

    Angular 22 introduce Zoneless por defecto, Signals y Control Flow nativo en templates. Mejora TTI y reduce renders innecesarios. Es una buena opción para equipos grandes y proyectos de larga duración; la migración requiere refactor y formación. Incluye la migración del stack de testing a Jest + Angular Testing Library como parte del plan.

    Angular 22 vs el resto: lo que cambia y por qué importa

    Zoneless por defecto

    Se abandona Zone.js. La detección de cambios pasa de ser global e impulsiva a ser controlada y granular. Resultado: TTI más bajo y menos renders innecesarios.

    Signals como primitivo de reactividad

    Reactividad sin subscriptions masivas. Signals reduce la boilerplate de RxJS para estado local y mejora predictibilidad.

    Control Flow nativo en templates (@if, @for)

    El compilador procesa control de flujo a nivel de AST, lo que incrementa rendimiento y legibilidad.

    Traducido: Angular ya compite en métricas de rendimiento con frameworks “reactivos” como Solid o con Vue, pero manteniendo un conjunto de herramientas integradas (DI, routing, forms) que otros frameworks dejan al ecosistema.

    Comparativa honesta: cuándo Angular gana y cuándo no

    Angular no es la mejor opción por defecto. Es la mejor opción para ciertos contextos.

    • Si ganas con opinión y consistencia: equipos de >10 devs, código con vida útil >3 años, requisitos de accesibilidad y compliance, Angular aporta coherencia y reduce decisiones ad-hoc.
    • Si priorizas libertad y prototipado rápido: React o Vue siguen siendo más ágiles. Next.js / Nuxt dominan en SSR/Server Components y experiencia híbrida contenido-aplicación.

    Arquitectura

    Angular = opinado; React = flexible; Vue = progresivo.

    Reactividad

    Angular = Signals; React = Hooks/Virtual DOM; Vue = Composition API.

    SSR y SEO

    Next.js/Nuxt > Angular Universal (mejora pero no centro de innovación).

    Mantenimiento en equipos grandes

    Angular > React (por la opinión y patrones forzados).

    Lo que nadie te cuenta sobre el coste real de migrar

    Hay dos costes que muchos subestiman.

    1) Coste técnico (trabajo manual)

    Actualizar con el CLI es el viaje fácil. El trabajo duro es refactorizar: pasar de NgModules a Standalone Components, reescribir flujos con Signals, adaptar templates a Control Flow nativo. Eso no se hace con sed. Es trabajo de diseño con pruebas y revisiones de arquitectura.

    2) Coste de conocimiento (formación)

    Si tu equipo maneja Angular 8–12 y nunca siguió la evolución, la migración se convierte en un proceso de aprendizaje. No es solo código; es cambiar patrones mentales.

    Estimación práctica: para un monorepo mediano (~50–100 paquetes) con Angular legacy, planifica entre 3–6 meses de esfuerzo de ingeniería + formación (una squad dedicada en paralelo). Para apps pequeñas, 2–4 semanas si controlas las dependencias.

    Testing: la parte que obliga a modernizar

    Karma y Jasmine están oficialmente deprecados. Seguir con ellos en 2026 equivale a cargar deuda técnica que ralentiza CI. El estándar actual es Jest + Angular Testing Library: tests por comportamiento, más rápidos y menos frágiles ante refactors.

    Si vas a migrar, incluye la migración del stack de testing en el plan. No lo dejes para “después”; las pruebas son el talón de Aquiles de cualquier transición grande.

    Plan de migración (práctico y priorizado)

    1. Auditoría de superficie: identifica paquetes que usan NgModules, transformadores de compilador o dependencias tightly-coupled.
    2. Formación y pilot: entrena 2–3 leads en Standalone + Signals. Ejecuta un pilot migrando un módulo crítico.
    3. Reescritura incremental: migrar componentes a Standalone, adaptar servicios a nuevo DI y sustituir RxJS local por Signals donde aplique.
    4. Testing first: antes de cambiar templates, adapta tests a Jest + Testing Library.
    5. Despliegue canario: canary en producción para un subset de usuarios. Monitorea TTI, errores y coste de CI.
    6. Feedback loop: métricas + sesiones de code review para homogeneizar patrones.

    Formación recomendada (práctica)

    Si tu equipo necesita acelerar la adopción, dos recursos útiles y prácticos:

    Ambos cubren desde conceptos arquitectónicos hasta patrones aplicables en migraciones reales.

    Conclusión: criterio para decidir en 2026

    Angular 22 es una opción sólida cuando necesitas previsibilidad, escalabilidad y uniformidad. No es una moda; es una apuesta por la ingeniería predecible. Pero la migración exige plan, formación y disciplina. Si tu prioridad es velocidad de lanzamiento inmediata y equipo pequeño, otras opciones siguen siendo más prácticas. Si en cambio trabajas en dominios donde la UI es infraestructura con vida útil larga —migrar a Angular 22 tiene sentido y trae beneficios tangibles: menos renders innecesarios, menos deuda y mejores métricas de rendimiento.

    Migrar no es solo actualizar dependencias. Es reescribir la forma en que piensas la UI. Hazlo con criterio.

    FAQ

    Respuesta: Zoneless significa que Angular deja de depender de Zone.js para la detección de cambios. La detección se vuelve más controlada y granular, lo que reduce renders innecesarios y mejora TTI.

    Respuesta: Signals son un primitivo de reactividad que permiten manejar estado local sin la sobrecarga de subscriptions masivas. Conviene usarlos para reducir boilerplate y mejorar la predictibilidad del estado local.

    Respuesta: Estimación práctica indicada en el artículo: para un monorepo mediano (~50–100 paquetes) planifica entre 3–6 meses de esfuerzo de ingeniería + formación con una squad dedicada en paralelo.

    Respuesta: Karma y Jasmine están deprecados; mantenerlos añade deuda técnica y ralentiza CI. Migrar a Jest + Angular Testing Library hace los tests más rápidos y menos frágiles ante refactors, y debe formar parte del plan de migración.

    Respuesta: Pilotear un módulo crítico con leads formados en Standalone + Signals es la recomendación: forma 2–3 leads y ejecuta un pilot antes de reescrituras a gran escala.

    Respuesta: En SSR y SEO Next.js/Nuxt suelen ofrecer una experiencia más avanzada; Angular Universal ha mejorado pero no es el centro de innovación en este espacio.