Category: JavaScript

  • 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.

  • Construyendo Agentes Rápidos con TypeScript y Vercel AI SDK

    Construyendo Agentes Rápidos con TypeScript y Vercel AI SDK

    TypeScript + Vercel AI SDK: la combinación que uso para construir agentes rápido

    Tiempo estimado de lectura: 4 min

    • Tipado + validación: TypeScript en la superficie y Zod en runtime reducen errores silenciosos y permiten refactors seguros.
    • API unificada: Vercel AI SDK conecta proveedores y ofrece streaming y herramientas tipadas.
    • Extracción y control: generateObject y esquemas evitan ingeniería de prompt frágil y JSON truncado.
    • UX y operaciones: streamText mejora la percepción de latencia; métricas y circuit breakers mantienen robustez en producción.

    TypeScript + Vercel AI SDK: la combinación que uso para construir agentes rápido. Si vas a poner agentes en producción, necesitas que la capa que conecta al LLM con tus herramientas sea predecible, tipada y validada desde el primer día. Esa combinación reduce errores silenciosos, acelera refactors y convierte promesas estocásticas en contratos verificables.

    Resumen rápido (lectores con prisa)

    TypeScript para tipado estático, Zod para validación en runtime y Vercel AI SDK como API unificada. Juntos: herramientas tipadas, extracción estructurada (generateObject), y streaming (streamText) para agentes más seguros y previsibles.

    TypeScript + Vercel AI SDK: por qué funciona para agentes rápidos

    Tres problemas recurrentes al construir agentes:

    1. El LLM alucina parámetros para las herramientas (tool calls)

    Los modelos pueden generar parámetros inválidos o inventados para llamadas a herramientas, lo que puede llevar a ejecuciones peligrosas si no se validan antes.

    2. Las respuestas JSON vienen envueltas en markdown o truncadas

    Solemos ver JSON con backticks, texto adicional o respuestas incompletas que complican el parsing confiable.

    3. Cambios en la API del proveedor rompen integraciones silenciosamente

    Actualizar modelos o proveedores puede introducir cambios incompatibles si no hay contratos y pruebas robustas.

    La solución práctica es simple: tipos en la superficie (TypeScript), contratos ejecutables (Zod) y una API que integra ambas cosas (Vercel AI SDK). Beneficios concretos:

    • Autocompletado que evita buscar docs.
    • Tool calls que no se ejecutan si los datos no validan.
    • Extracción de objetos estructurados (generateObject) sin ingeniería de prompt frágil.
    • Streaming nativo (streamText) para UX reactiva.

    Tool calls tipados: la barrera que evita ejecuciones peligrosas

    Definir herramientas con esquemas evita que el agente ejecute acciones con parámetros inventados. Ejemplo:

    import { tool } from 'ai';
    import { z } from 'zod';
    
    const searchOrders = tool({
      description: 'Busca pedidos por ID de cliente',
      parameters: z.object({
        customerId: z.string().uuid(),
        status: z.enum(['pending','shipped','delivered']).optional(),
      }),
      execute: async ({ customerId, status }) => {
        return queryOrdersDatabase({ customerId, status });
      },
    });
    

    Si el LLM devuelve un customerId inválido, Zod lo rechazará antes de llamar a execute. Resultado: menos excepciones en la base de datos y trazabilidad clara del fallo (prompt → validación → rechazo).

    generateObject: extracción fiable de datos estructurados

    generateObject obliga al modelo a respetar un esquema y te devuelve un objeto tipado sin hacer JSON.parse() manual. Ejemplo práctico:

    import { generateObject } from 'ai';
    import { openai } from '@ai-sdk/openai';
    import { z } from 'zod';
    
    const schema = z.object({
      sentiment: z.enum(['positive','neutral','negative']),
      confidence: z.number().min(0).max(1),
      topics: z.array(z.string()).max(5)
    });
    
    const { object } = await generateObject({
      model: openai('gpt-4o'),
      schema,
      prompt: 'Analiza la reseña y devuelve sentiment, confidence y topics.'
    });
    
    // object ya está tipado según schema
    

    Esto reduce la ingeniería de prompts (“Devuelve SOLO JSON”) y aumenta la tasa de respuestas utilizables desde el primer intento.

    streamText: UX que comunica progreso y permite pasos intermedios

    Los agentes suelen ejecutar varias herramientas en cadena. streamText permite emitir texto progresivo y reflejar estados intermedios (p. ej. “consultando base de datos…”) en la UI sin arquitectura adicional:

    • Emite tokens progresivamente al frontend.
    • Reporta eventos de invocation/execute de herramientas.
    • Funciona tanto en Server (Next.js) como en cliente con hooks (useChat).

    Esto mejora la percepción de latencia y permite interacciones más naturales con agentes multi‑paso.

    Integración práctica y operaciones en producción

    Patrón recomendado

    1. Diseña esquemas Zod como fuente única de verdad.
    2. Expón el esquema (o ejemplo) en el prompt para guiar al LLM.
    3. Usa safeParse() para reintentos y autocorrección de prompts; usa parse() para endpoints que deben fallar rápido.
    4. Loguea prompt, raw response y error de Zod (flatten) para trazabilidad.

    Medidas operativas

    • Métricas: tasa de validación fallida, latencia media por herramienta, reintentos por prompt.
    • Retries limitados con backoff y contador de intentos (p. ej. 2 reintentos de autocorrección antes de degradar a humano).
    • Circuit breaker para evitar invocar herramientas costosas si la validación falla en cascada.

    Limitaciones y decisions trade‑offs

    • No eliminas la estocasticidad del LLM; la controlas. Algunos casos requerirán supervisión humana.
    • generateObject y Structured Outputs reducen errores de formato, pero no sustituyen la validación semántica (p. ej. números positivos). Zod sigue siendo necesaria.
    • Tipar desde el día 0 impone disciplina, pero acelera onboarding y refactors.

    Conclusión

    TypeScript + Vercel AI SDK: la combinación que uso para construir agentes rápido no es un truco de marketing. Es una estrategia concreta: tipos para detectar cambios, Zod para validar en runtime, y un SDK que une proveedores, streaming y herramientas tipadas. Si tu objetivo es desplegar agentes que actúen sobre sistemas reales—bases de datos, pedidos, o infraestructuras—esta pila reduce fallos silenciosos y convierte iteración rápida en ingeniería sostenible.

    Para equipos que exploran automatización y agentes como flujo de trabajo productivo, una guía práctica y recursos adicionales están disponibles en Dominicode Labs. Es una continuación lógica para quienes quieren aterrizar estas prácticas en sistemas reales.

    FAQ

    ¿Por qué combinar TypeScript con Zod y un SDK como Vercel AI SDK?

    TypeScript aporta seguridad estática y autocompletado; Zod proporciona validación en runtime; y Vercel AI SDK unifica la interacción con proveedores, streaming y herramientas tipadas. La combinación reduce errores silenciosos y facilita refactors.

    ¿Cómo evitan las herramientas tipadas ejecuciones peligrosas?

    Al definir parámetros con esquemas Zod, cualquier dato que no valide se rechaza antes de ejecutar la función execute, evitando operaciones con parámetros inventados o inválidos.

    ¿Qué ventaja ofrece generateObject frente a parsear JSON manualmente?

    generateObject obliga al modelo a respetar un esquema y devuelve un objeto ya tipado, evitando la ingeniería de prompt para forzar JSON y reduciendo errores por markdown, texto adicional o truncado.

    ¿Cuándo debo usar streamText?

    Cuando quieras mejorar la UX en interacciones multi‑paso: emitir tokens progresivamente, mostrar estados intermedios y reportar eventos de invocation/execute sin añadir complejidad arquitectónica.

    ¿Qué métricas operativas son críticas?

    Métricas como tasa de validación fallida, latencia media por herramienta y reintentos por prompt son esenciales para monitorear la salud y eficacia del agente.

    ¿Cuáles son las limitaciones principales de esta pila?

    No elimina la estocasticidad del LLM; solo la controla. También requiere validación semántica adicional (p. ej. asegurar números positivos). Tipar desde el día 0 impone disciplina, aunque acelera onboarding y refactors.

  • Errores comunes al migrar a React Server Components en producción

    Errores comunes al migrar a React Server Components en producción

    React Server Components en producción: errores que nadie te cuenta

    Tiempo estimado de lectura: 5 min

    • Fronteras claras: mezclar datos pesados del servidor con Client Components provoca serialización y payloads enormes.
    • No convertir todo a client: usar “use client” globalmente anula los beneficios de RSC y regresa a una SPA pesada.
    • Latencia y caching: llamadas secuenciales y caché agresiva generan TTFB alto y fugas de datos entre usuarios.
    • Audita dependencias: muchas librerías no están preparadas para ejecución en server; lazy-load o wrappers client son necesarios.

    Introducción

    React Server Components en producción: errores que nadie te cuenta. Lo digo sin rodeos: los tutoriales y demos no te preparan para operarlos en tráfico real. En ese salto es donde aparecen fugas de datos, payloads monstruosos y cuellos de botella invisibles que desarman la promesa de “menos JS, mejor rendimiento”.

    Este artículo enumera los fallos concretos que verás en proyectos reales, aporta soluciones técnicas y define cuándo NO migrar a RSC. Incluye referencias y enlaces oficiales para que puedas profundizar: Suspense y caching en Next.js.

    Resumen rápido (lectores con prisa)

    Qué es: Patrón que permite renderizar parte de la UI en el servidor y enviar un árbol serializado al cliente.

    Cuándo usarlo: cuando puedas controlar la frontera server/client, minimizar datos pasados al cliente y beneficiarte de menos JS inicial.

    Por qué importa: mejora rendimiento y seguridad si se adopta con disciplina en serialización, caché y orquestación de datos.

    Cómo funciona: Server Components pueden acceder a recursos de servidor; Client Components se hidratan en cliente y deben recibir solo datos mínimos.

    1. React Server Components en producción: la frontera que rompe todo

    El error raíz es conceptual: tratar la frontera server/client como una línea estética en lugar de una decisión arquitectónica. Un Server Component puede acceder a la BD y luego pasar objetos enormes como props a un Client Component. Eso obliga a React a serializar todo en el HTML/JSON de respuesta. Resultado: la reducción del bundle se convierte en megabytes de payload.

    Ejemplo típico (malo)

    • Server Component hace SELECT * FROM orders WHERE user_id = ? y pasa todos los registros a <OrdersTable use client />.
    • El navegador recibe un payload serializado de decenas de MB.

    Solución: procesar, paginar y resumir en el servidor. Pasa al cliente solo el minimum viable (IDs, count, primeros N items) y provee endpoints client-side para cargar la página de datos al interactuar.

    2. El pánico del “use client” y la regresión a SPA

    Cuando algo falla (proveedores, librerías de UI, hooks), el atajo más común es colocar "use client" en el layout. Eso convierte todo el árbol en Client Components y anula el beneficio de RSC: vuelves a una SPA grande, con mayor complejidad y sin reducción de JS.

    Patrón correcto:

    • Mantén providers y estado en componentes hoja que realmente necesitan interactividad.
    • Diseña la composición para que los Client Components reciban props mínimos y, si requieren datos pesados, llamen a endpoints específicos (fetch desde cliente) o utilicen streaming.

    3. Waterfalls invisibles: el backend secuencial que mata TTFB

    Código like-this en Server Component:

    const user = await getUser(id);
    const prefs = await getPrefs(user.configId);
    const orders = await getOrders(user.id);
    

    Eso es secuencial: suma latencias. Aunque ocurre en servidor, el usuario espera. Paraleleza con Promise.all cuando no hay dependencia, y usa Suspense para streaming progresivo cuando sí hay dependencias parciales.

    Patrón secuencial y solución

    • Identifica llamadas independientes y ejecútalas en paralelo.
    • Usa streaming y Suspense para mostrar partes de la vista cuando están listas.
    • Mide TTFB en staging bajo carga para detectar waterfalls invisibles.

    4. Caché agresiva = fuga de datos entre usuarios

    Next.js y otros frameworks aplican caching por defecto en render server. Si renderizas una ruta con datos privados y no marcas la petición como dinámica, puedes cachear la vista de un usuario y servirla a otro. Es real y está pasando en producción.

    Contramedidas:

    • Para datos privados usa { cache: 'no-store' } en fetch o llama a APIs que leen cookies()/headers() (esto fuerza render dinámico en Next.js).
    • Revisa la documentación de caché de Next.js: caching en Next.js.
    • Considera políticas CDN más conservadoras para rutas autenticadas.

    5. Integraciones de terceros que no están listas para server execution

    Muchas librerías npm asumen un entorno DOM. Al ejecutar en server, aparecen errores en build o comportamiento inesperado. Resultado: el equipo marca "use client" masivo y pierde las ventajas. Revisa dependencias: algunas requieren reemplazo o lazy-loading estricto.

    Táctica práctica:

    • Audita las dependencias con npm ls y pruebas de build en CI que marquen dónde fallan.
    • Si una librería solo se usa en un widget, envuélvela en un Client Component lazy-loaded.

    Cuándo NO usar React Server Components

    No migres a RSC si tu producto encaja en alguno de estos casos:

    • Aplicaciones offline-first o PWAs que deben funcionar sin servidor.
    • Interfaces de hiper-interactividad: editores gráficos, juegos, vídeo en tiempo real o UIs con WebSockets a alta frecuencia.
    • Bases de código legacy sin presupuesto de reescritura: migrar Redux heavy/class components = reescritura, no refactor.

    Checklist práctico antes de migrar a producción

    1. Delimita claramente boundaries: quién corre en server y qué mínima data pasa al cliente.
    2. Añade tests de integración que simulen carga y validen payloads.
    3. Forza políticas de cache por ruta (privada vs pública).
    4. Instrumenta logs de tamaño de respuesta y tokenización/serialización.
    5. Adopta streaming/Suspense para vistas complejas; usa Promise.all para llamadas paralelas.
    6. Audita dependencias y evita convertir el layout en client por comodidad.

    Conclusión: RSC exige disciplina, no solo adopción

    React Server Components entregan ventajas claras (menos JS inicial, mayor seguridad para secretos, mejor SEO). Pero funcionan en producción solo si el equipo re-aprende backend: serialización, caching, latencia y orquestación de datos. La migración exitosa no es técnica aislada; es un cambio de modelo mental: pasar de “componentes” a “árboles de dependencias de red”. Si no estás dispuesto a trazar fronteras con rigor, no migres: estarás complicando tu arquitectura sin ganar sus beneficios.

    FAQ

    ¿Qué es exactamente un React Server Component?

    Un React Server Component se renderiza en el servidor y puede acceder a recursos del backend. No se hidrata en el cliente como un Client Component y se envía serializado al navegador.

    ¿Cuándo debo evitar migrar a RSC?

    Evita migrar si necesitas soporte offline completo, tienes UIs de hiper-interactividad (editores, juegos, video en tiempo real) o una base de código legacy sin presupuesto para reescritura.

    ¿Cómo evito pasar payloads gigantes al cliente?

    No pases objetos completos como props. Resumir, paginar y enviar solo lo mínimo necesario (IDs, count, primeros N items). Usa endpoints client-side para cargar datos adicionales bajo demanda.

    ¿Qué problemas de caché debo vigilar en Next.js?

    Cuidado con el render estático por defecto: rutas con datos privados pueden quedar cacheadas. Para datos privados usa { cache: 'no-store' } en fetch o APIs que lean cookies()/headers() para forzar render dinámico.

    ¿Cómo detectar y arreglar waterfalls en Server Components?

    Mide TTFB en staging bajo carga, revisa llamadas secuenciales en Server Components y paraleliza con Promise.all cuando sea posible. Usa streaming y Suspense para render progresivo.

    ¿Qué hago con librerías que fallan en server?

    Audita dependencias con npm ls, añade pruebas de build en CI y envuelve las librerías problemáticas en Client Components lazy-loaded o busca alternativas compatibles con server execution.