Tag: JavaScript

  • Por qué Bun está reemplazando a Node.js en backend con TypeScript: Rendimiento y DX sin bundlers

    Por qué Bun está reemplazando a Node.js en backend con TypeScript: Rendimiento y DX sin bundlers

    Hace un tiempo migramos la suite de microservicios e integraciones de un proyecto en backend desde Node.js hacia Bun.

    En el entorno anterior basado en Node.js, la cadena de herramientas (toolchain) incluía tsx para desarrollo local, esbuild para compilar TypeScript a JavaScript antes de desplegar, jest para pruebas unitarias y npm para gestionar paquetes. Instalar dependencias en CI tardaba 35 segundos. El arranque del servidor de desarrollo tomaba 4.2 segundos.

    Al migrar a Bun:

    • bun install redujo el tiempo de instalación a 650 milisegundos.
    • bun run dev arrancó el servidor de TypeScript de forma instantánea (0 ms).
    • bun test ejecutó 300 tests unitarios en 1.2 segundos (frente a los 14 segundos de Jest).

    Bun no es simplemente un ejecutor de JavaScript alternativo; es una navaja suiza que reemplaza de un plumazo a Node.js, npm, ts-node, esbuild, Vite y Jest en aplicaciones backend con TypeScript.

    La diferencia arquitectónica: V8 vs. JavaScriptCore y Zig

    Node.js y Deno están construidos sobre el motor V8 de Google (escrito en C++).

    Bun fue desarrollado desde cero por Jarred Sumner utilizando el lenguaje de programación Zig y el motor JavaScriptCore (JSC) desarrollado por Apple para Safari.

    Esta decisión de diseño otorga a Bun ventajas competitivas clave:

    1. Menor tiempo de arranque (Cold Starts): JavaScriptCore arranca y genera bytecode significativamente más rápido que V8, lo que convierte a Bun en el runtime idóneo para funciones Serverless e integraciones con IA.
    2. Uso de memoria optimizado: El recolector de basura de JSC gestiona los objetos de heap con menor consumo de RAM en estado inactivo.
    3. Manejo de archivos a nivel de kernel: Operaciones I/O de lectura de archivos (Bun.file()) y llamadas de red HTTP son procesadas mediante llamadas de sistema de bajo nivel en Zig.
    ┌─────────────────────────────────────────────────────────┐
    │ Ecosistema Node.js Tradicional                          │
    │ ┌───────────────┬───────────────┬─────────────────────┐ │
    │ │ Node.js (V8)  │ npm / pnpm    │ ts-node / esbuild   │ │
    │ └───────────────┴───────────────┴─────────────────────┘ │
    └──────────────────────────┬──────────────────────────────┘
                               │ REEMPLAZADO POR:
    ┌──────────────────────────▼──────────────────────────────┐
    │ Runtime Bun (Zig + JavaScriptCore)                      │
    │ ┌─────────────────────────────────────────────────────┐ │
    │ │ Ejecución nativa de TypeScript / JSX sin transpilar  │ │
    │ ├─────────────────────────────────────────────────────┤ │
    │ │ Bundler + Test Runner + Package Manager (bun.lockb) │ │
    │ ├─────────────────────────────────────────────────────┤ │
    │ │ Driver SQLite/Postgres nativo + WebSockets + Servidor│ │
    │ └─────────────────────────────────────────────────────┘ │
    └─────────────────────────────────────────────────────────┘
    

    3 Características que Cambian la Experiencia de Desarrollo (DX)

    1. Ejecución nativa de TypeScript sin transpiladores externos

    En Node.js, para ejecutar un archivo .ts, necesitas configurar un transpilador como ts-node, tsx o compilar primero a una carpeta dist/ con tsc.

    En Bun, ejecutas directamente:

    bun run src/index.ts
    

    Bun transquila al vuelo archivos .ts, .tsx, .js y .jsx en memoria C++ sin requerir archivos tsconfig complejos ni configuraciones de Build.

    2. Gestor de paquetes ultrarrápido (bun install)

    bun install utiliza llamadas de sistema de copiado de memoria en Linux/macOS (copy-on-write) y un formato de lockfile binario (bun.lockb).

    Instalar un paquete como zod o hono toma milisegundos porque Bun aprovecha un sistema de caché global compartido entre todos tus proyectos locales.

    3. API HTTP y WebSockets nativas de alto rendimiento

    Servir peticiones HTTP en Bun requiere un bloque de código mínimo sin depender de Express o Fastify:

    import { serve } from "bun";
    
    serve({
      port: 3000,
      fetch(req) {
        const url = new URL(req.url);
        if (url.pathname === "/api/health") {
          return Response.json({ status: "ok", runtime: "Bun 1.2" });
        }
        return new Response("Not Found", { status: 404 });
      },
      websocket: {
        message(ws, message) {
          ws.send(`Eco: ${message}`);
        },
      },
    });
    

    Al combinar este rendimiento con los principios de programación defensiva en TypeScript, construyes servidores web robustos que responden en microsegundos y resisten picos de tráfico masivos.

    Comparativa con Next.js y Turbopack

    Como analizamos en nuestro informe sobre la optimización de memoria en Next.js y Turbopack, el consumo de recursos en herramientas de desarrollo ha sido un dolor constante para los desarrolladores.

    Bun soluciona este problema desde la raíz: consume hasta un 60% menos de memoria RAM durante la compilación y ejecución que Node.js con Webpack o Vite.

    Además, al simplificar la arquitectura de dependencias siguiendo buenas prácticas de graph engineering, tus repositorios se vuelven más fáciles de mantener tanto para desarrolladores como para agentes de IA.


    Bun no es el futuro del desarrollo backend en TypeScript; es el presente en proyectos de alta velocidad.

    Si quieres dominar el desarrollo backend moderno, microservicios y mejores prácticas de arquitectura con TypeScript y Bun, explora los Cursos de Dominicode. Y si quieres colaborar en proyectos reales junto a desarrolladores senior, súmate a Dominicode Labs.

    Preguntas frecuentes

    ¿Bun es compatible con el ecosistema existente de npm y módulos de Node.js?

    Sí. Bun implementa soporte para las APIs globales de Node.js (fs, path, http, stream, buffer) y soporta la importación de módulos tanto CommonJS (require) como ESM (import). La inmensa mayoría de paquetes de npm (incluyendo Prisma, Drizzle, Hono, NestJS y Express) funcionan de forma transparente en Bun.

    ¿Se puede utilizar Bun para producción en servidores Linux?

    Absolutamente. Bun cuenta con soporte de producción completo para entornos Linux x64 y ARM64. Grandes plataformas de despliegue como Vercel, Fly.io, Railway y AWS Lambda ofrecen ejecución nativa de aplicaciones basadas en Bun.

    ¿Cómo funciona bun test en comparación con Jest o Vitest?

    bun test es un test runner compatible con la sintaxis de Jest (describe, it, expect, beforeEach). Al estar integrado directamente en el runtime en lenguaje Zig, ejecuta suites de tests hasta 10 veces más rápido que Jest y 3 veces más rápido que Vitest sin requerir plugins adicionales.

    ¿Debo migrar todos mis proyectos existentes de Node.js a Bun de inmediato?

    Para proyectos nuevos, microservicios, scripts de automatización e integraciones con IA, Bun es la recomendación número uno. Para proyectos legacy de gran tamaño en Node.js, se sugiere comenzar migrando primero la ejecución de bun install y bun test en tus pipelines de CI/CD para ganar velocidad de inmediato antes de cambiar el runtime de producción.


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

  • Visor de PDF en Next.js con Apryse WebViewer: guía real

    Visor de PDF en Next.js con Apryse WebViewer: guía real

    Un cliente me escribió con lo que parecía un encargo de dos semanas.

    «Necesitamos que el usuario abra su contrato dentro de la plataforma, lo anote, tache los datos sensibles y lo firme. Sin descargar nada, sin salir de la app.»

    Dos semanas. Claro.

    Lo que estaba pidiendo era un visor de PDF en Next.js con capa de anotaciones persistentes, redacción de verdad —no un rectángulo negro pintado encima, sino borrar el texto del documento— y firma. Es decir: un producto entero metido dentro de una ruta de la aplicación.

    Lo he visto intentar a mano tres veces. Las tres acabaron igual: seis meses después seguían peleando con fuentes embebidas y zoom en móvil.


    Resumen: visor de PDF en Next.js en 5 puntos

    • Un visor de PDF en Next.js es un componente de cliente que renderiza documentos dentro de tu aplicación, sin descargarlos ni delegar en el visor nativo del navegador. Requiere dos cosas que no son obvias: cargar la librería solo en el navegador y servir sus assets estáticos desde public/.
    • Construir a mano anotaciones, redacción y firma sobre PDF es un pozo sin fondo: el formato tiene 30 años de casos borde.
    • Apryse WebViewer (@pdftron/webviewer) te da ese flujo completo en el navegador, sin backend de por medio.
    • Es un SDK comercial con trial gratuito. Los paquetes de entrada arrancan en $1.500 según su web de precios —consultado en julio de 2026—, y el precio final es a medida.
    • Si solo necesitas mostrar un PDF, no lo uses. pdf.js o react-pdf te resuelven eso gratis.

    Por qué "solo un visor de PDF" nunca es solo un visor

    Un PDF no es una imagen con texto: es un contenedor con tipografías embebidas, capas, formularios AcroForm, XFA, firmas criptográficas, anotaciones con su propio modelo de datos y treinta años de decisiones heredadas. Parece simple solo porque lo abres todos los días y funciona.

    Renderizar la primera página con pdf.js te lleva una tarde. El problema empieza después.

    Que la anotación quede anclada al párrafo correcto al hacer zoom. Que la redacción elimine el texto del stream y no solo lo tape —si lo tapas, cualquiera lo copia con Ctrl+C y tienes un incidente de datos—. Que la firma se incruste sin romper la validez del documento. Y que todo eso funcione igual en Safari iOS.

    Ese es el trabajo real. Y no es trabajo de dos semanas: es trabajo de un equipo dedicado durante trimestres.

    Antes de meter una pieza así en tu aplicación, escribe qué necesitas exactamente. Suena obvio y casi nadie lo hace. En cómo aplico Spec-Driven Development antes de escribir código explico el proceso —y lo tienes entero en el libro de Spec-Driven Development—. Esta es justo la decisión donde una especificación de una página te evita elegir mal una licencia comercial.


    Qué es Apryse WebViewer y qué te ahorra

    Apryse WebViewer es un SDK comercial que monta un visor y editor de PDF en React dentro de tu aplicación, ejecutándose entero en el navegador. No necesitas un servicio de conversión detrás.

    Lo que trae de fábrica:

    • Anotación completa: resaltados, notas, dibujo, formas, comentarios.
    • Redacción real, que elimina el contenido del documento.
    • Edición de texto sobre el PDF.
    • Fill & sign para formularios y firma.
    • Búsqueda dentro del documento.
    • Cambio programático del documento cargado.
    • Soporte de más de 100 formatos: Office, imágenes, CAD. No solo PDF.

    Ese último punto suele cerrar la decisión. Cuando el cliente añade «ah, y también suben Word y planos», ya no evalúas un visor: evalúas si escribes tu propio pipeline de conversión.


    Cómo montar un visor de PDF en Next.js paso a paso

    La guía oficial de Apryse para Next.js cubre la instalación y es correcta. Lo que sigue añade lo que no te cuenta: la limpieza al desmontar, el fallo silencioso cuando path está mal y en qué casos no deberías estar leyendo este tutorial.

    1. Instalar el paquete

    npm i @pdftron/webviewer@^12
    

    Fijar la mayor te evita que un npm update te cambie la API por debajo. Este tutorial está escrito sobre la rama 12 con Next.js 16 y App Router.

    2. Copiar los assets estáticos

    Este es el paso que más gente se salta y luego pasa una tarde mirando 404 en la pestaña de red. WebViewer necesita sus propios archivos servidos estáticamente: workers, fuentes, recursos de UI.

    npx --yes cpy-cli "node_modules/@pdftron/webviewer/public/**/*" public/lib/webviewer
    

    Guárdalo como script de postinstall. Si no lo haces, funcionará en tu máquina y fallará en el primer despliegue limpio de CI:

    {
      "scripts": {
        "postinstall": "npx --yes cpy-cli \"node_modules/@pdftron/webviewer/public/**/*\" public/lib/webviewer"
      }
    }
    

    3. El componente, siempre en cliente (y el error "window is not defined")

    WebViewer toca window y el DOM en el momento de inicializarse. Si Next.js intenta renderizarlo en el servidor, revienta con el clásico ReferenceError: window is not defined.

    Aquí está el matiz que atasca a casi todo el mundo: marcar el componente con 'use client' no basta si el import es estático. Esa directiva define dónde se hidrata el componente, no impide que el módulo se evalúe al construir el bundle del servidor.

    La solución es importar el módulo dinámicamente dentro del useEffect, para que solo se resuelva en el navegador después del montaje.

    'use client'
    import { useEffect, useRef } from 'react'
    
    export default function PdfViewer() {
      const viewer = useRef(null)
    
      useEffect(() => {
        const container = viewer.current
        let instance = null
        let unmounted = false
    
        const destroy = () => {
          instance?.UI?.dispose?.()
          instance = null
          if (container) container.innerHTML = ''
        }
    
        import('@pdftron/webviewer')
          .then(({ default: WebViewer }) =>
            WebViewer(
              {
                path: '/lib/webviewer',
                licenseKey: process.env.NEXT_PUBLIC_APRYSE_LICENSE_KEY,
                initialDoc: 'https://apryse.s3.amazonaws.com/public/files/samples/WebviewerDemoDoc.pdf',
              },
              container,
            ),
          )
          .then((i) => {
            instance = i
            if (unmounted) return destroy()
            i.Core.documentViewer.addEventListener('documentLoaded', () => {
              // el documento ya está en pantalla: engancha aquí tu lógica
            })
          })
          .catch((error) => {
            console.error('WebViewer no arrancó. Revisa la opción `path`:', error)
          })
    
        return () => {
          unmounted = true
          destroy()
        }
      }, [])
    
      return <div ref={viewer} style={{ height: '100dvh' }} />
    }
    

    Cuatro detalles que conviene entender, no copiar:

    • path apunta exactamente a donde copiaste los assets en el paso 2. Si cambias la carpeta, cambia esto.
    • El array de dependencias vacío no te salva de StrictMode. En desarrollo React monta, desmonta y vuelve a montar: el efecto corre dos veces y, sin función de limpieza, acabas con dos visores peleando por el mismo div. Por eso el return del useEffect llama a UI.dispose() y vacía el contenedor. Es la parte que casi ningún tutorial escribe.
    • La bandera unmounted cubre el otro orden posible: que el usuario navegue a otra ruta antes de que resuelva el import() dinámico. Sin ella te quedas un iframe y unos workers WASM vivos en memoria.
    • La promesa resuelve con un instance que expone instance.Core —donde vive documentViewer— e instance.UI. Ese objeto es tu mando a distancia.

    Y sí, el .catch() importa: si path está mal, sin él la promesa se rechaza en silencio y te quedas mirando un div en blanco sin una sola pista en consola.

    Si necesitas la API completa, añade fullAPI: true a las opciones.

    La clave va en variable de entorno para no hardcodearla en el repo:

    NEXT_PUBLIC_APRYSE_LICENSE_KEY=tu_clave_de_trial
    

    Ojo con la etiqueta: cualquier variable NEXT_PUBLIC_ viaja al bundle del navegador, así que esto no la convierte en un secreto. En WebViewer la licencia es de cliente y va ligada a tu dominio, así que es correcto y esperado; pero si algún día metes aquí una credencial de verdad, ese secreto vive en tu backend, no en una NEXT_PUBLIC_.


    Controlar el visor desde tu propia interfaz

    Casi nadie quiere la UI del SDK tal cual: quieres tus botones, con tu marca, en tu layout.

    El patrón es guardar la instancia en estado o en una ref y llamar a sus métodos desde tus componentes. El visor deja de ser una caja negra y pasa a ser un motor que tú comandas.

    'use client'
    import { useEffect, useRef, useState } from 'react'
    
    export default function PdfWorkspace() {
      const viewer = useRef(null)
      const [instance, setInstance] = useState(null)
    
      useEffect(() => {
        const container = viewer.current
        let current = null
        let unmounted = false
    
        const destroy = () => {
          current?.UI?.dispose?.()
          current = null
          setInstance(null)
          if (container) container.innerHTML = ''
        }
    
        import('@pdftron/webviewer')
          .then(({ default: WebViewer }) =>
            WebViewer(
              {
                path: '/lib/webviewer',
                licenseKey: process.env.NEXT_PUBLIC_APRYSE_LICENSE_KEY,
                fullAPI: true,
              },
              container,
            ),
          )
          .then((i) => {
            current = i
            if (unmounted) return destroy()
            setInstance(i)
          })
          .catch((error) => {
            console.error('WebViewer no arrancó:', error)
          })
    
        return () => {
          unmounted = true
          destroy()
        }
      }, [])
    
      return (
        <>
          <button
            disabled={!instance}
            onClick={() => {
              if (!instance) return
              const { documentViewer } = instance.Core
              // llama aquí al método que necesites sobre el documento
            }}
          >
            Acción propia
          </button>
          <div ref={viewer} style={{ height: '100dvh' }} />
        </>
      )
    }
    

    Un aviso de rendimiento: este bundle es grande. No lo cargues en el layout raíz ni en una ruta que la gente visita de paso. Aíslalo en su propia ruta y deja que el router precargue lo demás — hablé de esto al analizar las navegaciones instantáneas de Next.js 16.3, y aquí la diferencia entre hacerlo bien y mal se nota en segundos, no en milisegundos.


    La parte incómoda: cuánto cuesta Apryse WebViewer

    Apryse WebViewer no es gratis: su web de precios indica paquetes de entrada desde $1.500, y el precio final es a medida. Esa es la cifra, y aquí es donde la mayoría de tutoriales se callan.

    El importe depende de las features que actives, del volumen de documentos y de si la solución es cliente o servidor. No hay tarifa pública por tramos: hay que pedir presupuesto.

    Antes de pagar puedes probarlo. Apryse ofrece un trial gratuito, y su documentación de instalación te pide obtener una trial key en el portal de desarrolladores como paso previo. No te fíes de las condiciones que leas en un blog —incluido este—: mira el portal, que es donde cambian.

    Vas a encontrar otras cifras circulando por foros y comparativas. Ninguna sale de Apryse. Ignóralas y pide presupuesto: es la única cifra que vale para tu caso.


    ¿Cuándo NO usar Apryse?

    Si lo único que necesitas es mostrar un PDF en modo lectura, esto es un cañón para matar una mosca.

    Para eso tienes pdf.js de Mozilla, o react-pdf —construido encima— si quieres la integración con componentes ya resuelta. Son gratis, open source, maduros y te resuelven ese caso entero. Meter un SDK comercial ahí es quemar presupuesto y añadir peso al bundle sin ganar nada.

    Esta es la comparativa que a mí me habría ahorrado dos días de evaluación:

    Necesidad pdf.js react-pdf Apryse WebViewer
    Renderizar, paginar, zoom
    Búsqueda en el documento ⚠️ manual
    Componentes React listos
    Anotaciones persistentes
    Redacción real (borra del stream)
    Edición de texto sobre el PDF
    Fill & sign / firma
    Office, imágenes, CAD (100+ formatos)
    Licencia Apache 2.0 MIT Comercial
    Coste Gratis Gratis Desde $1.500, a medida

    Léela en diagonal y verás el patrón: las tres primeras filas son un empate, y todo lo demás es una columna sola. Si tu requisito vive en las tres primeras filas, ya tienes tu respuesta y es gratis.

    Apryse gana en un escenario concreto: cuando el documento es parte del flujo de negocio. Cuando el usuario tiene que anotar, redactar, rellenar, firmar o editar, y ese flujo es lo que el cliente está pagando.

    Y ahí el cálculo no es "SDK caro contra librería gratis". Es esto: cuántos meses de ingeniería cuesta construir y mantener redacción, anotaciones y firma bien hechas, contra el precio de licenciarlo.

    Cuando lo planteas así, la respuesta suele ser evidente. El coste no es el SDK. Es el tiempo que no gastas.


    Qué hacer hoy

    Instala el trial, copia los assets, monta el componente de arriba y ábrelo con un PDF real de tu cliente. No el de ejemplo: uno feo, escaneado, de 80 páginas.

    En veinte minutos sabrás si esto resuelve tu problema o si te sobra con react-pdf. Esa decisión, tomada con el visor delante y no leyendo comparativas, vale más que cualquier post.

    Y si lo que quieres es integrar piezas grandes como esta sin perder tres días leyendo documentación, ese es exactamente el flujo que enseño en el curso Construye con IA: especificar primero, delegar la integración después. La metodología completa está en el libro de Spec-Driven Development, y si prefieres hacerlo acompañado, en Dominicode Labs trabajamos integraciones como esta sobre proyectos reales.


    Preguntas frecuentes

    ¿Apryse WebViewer es gratis?

    No. Es un SDK comercial. Sí ofrece un trial gratuito para validar si encaja con tu caso: su documentación de instalación te pide obtener una trial key en el portal de desarrolladores antes de empezar. Para producción, su web de precios indica paquetes de entrada desde $1.500 (consultado en julio de 2026), con precio final a medida según las features que actives, el volumen de documentos y si el despliegue es en cliente o en servidor. Las condiciones exactas del trial las marca su portal, no los blogs.

    ¿Se puede usar Apryse WebViewer con el App Router de Next.js?

    Sí, con dos condiciones. El componente que lo monta debe llevar la directiva 'use client' y el import del paquete tiene que ser dinámico dentro del useEffect, no estático en la cabecera del archivo. Así el bundle del servidor nunca evalúa el SDK. Además tienes que copiar los assets estáticos del paquete a public/lib/webviewer y apuntar la opción path a esa ruta.

    ¿Por qué me da el error "window is not defined" al integrar el visor?

    Porque Next.js está intentando ejecutar el SDK durante el renderizado en servidor, donde no existe el objeto window. Marcar el componente como cliente no basta si el import es estático: el módulo se evalúa igualmente al construir. La solución es cargarlo con import('@pdftron/webviewer') dentro del useEffect, de forma que solo se resuelva en el navegador después del montaje.

    ¿Qué alternativa gratuita hay a Apryse?

    pdf.js de Mozilla y react-pdf, que está construido encima. Ambos son open source y cubren perfectamente la visualización de documentos: renderizado, paginación, zoom y búsqueda básica. Donde no llegan es en el flujo completo de trabajo con documentos —redacción que borra contenido de verdad, edición de texto, fill & sign, anotaciones persistentes con su modelo de datos—. Si tu requisito es leer, usa las gratuitas. Si tu requisito es operar sobre el documento, compara con Apryse.

    ¿Sirve solo para PDF?

    No. WebViewer soporta más de 100 formatos, incluidos documentos de Office, imágenes y archivos CAD, y los renderiza en el mismo visor sin necesidad de un servicio de conversión en el servidor. Suele ser el factor decisivo cuando los usuarios suben lo que tienen a mano y no un PDF bien generado.


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

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

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

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

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

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

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

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

    ¿Qué es Nuxt 4?

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

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

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

    Estructura de directorios en Nuxt 4: la carpeta app/

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

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

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

    Así queda un proyecto:

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

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

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

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

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

    Crear el proyecto y arrancarlo

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

    Para crear el proyecto:

    npm create nuxt@latest mi-proyecto
    

    Con pnpm:

    pnpm create nuxt@latest mi-proyecto
    

    Con bun:

    bun create nuxt@latest mi-proyecto
    

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

    npm run dev -- -o
    

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

    Lo que vas a usar todos los días

    Rutas por ficheros

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

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

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

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

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

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

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

    Data fetching: $fetch vs useFetch vs useAsyncData

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

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

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

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

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

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

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

    Rutas de servidor

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

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

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

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

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

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

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

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

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

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

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

    Novedades de Nuxt 4.5

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

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

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

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

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

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

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

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

    const layout = useLayout()
    

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

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

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

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

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

    Cuándo Nuxt tiene sentido y cuándo no

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

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

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

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

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

    Qué hacer hoy

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

    npx codemod@latest nuxt/4/file-structure
    

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

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

    Preguntas frecuentes

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

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

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

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

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

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

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

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

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

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

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

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

    ¿Necesito crear la carpeta app/pages/ siempre?

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

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

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

    ¿Nuxt 4 usa TypeScript por defecto?

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


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

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

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

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

  • Resource API en Angular 22: el fin del subscribe() manual

    Resource API en Angular 22: el fin del subscribe() manual

    Revisé hace poco un componente de Angular que cargaba una lista de productos. Contaba los observables con los dedos: un BehaviorSubject para la categoría seleccionada, un switchMap hacia HttpClient, un catchError para los fallos, un takeUntilDestroyed para no dejar suscripciones vivas, y al final, un async pipe en el template.

    Todo correcto. Todo necesario. Y todo código que explica exactamente lo mismo que cualquier otro componente de carga de datos en la aplicación. La Resource API de Angular 22 resuelve exactamente este problema.

    Ese patrón tiene quince años. Angular 22 tiene la respuesta definitiva.

    Qué es la Resource API y por qué existe

    La Resource API es el mecanismo nativo de Angular para gestionar operaciones asíncronas dentro del sistema de Signals. No es una librería de terceros, no es un wrapper sobre RxJS: es la pieza que faltaba para que el modelo reactivo de Angular estuviera completo.

    La idea central es sencilla: tienes un signal que representa un parámetro (un ID, un filtro, una página), y quieres que Angular haga automáticamente el fetch cuando ese parámetro cambia. Sin subscribe, sin pipe, sin gestión manual del ciclo de vida.

    En Angular 22 el ecosistema completo se compone de tres APIs:

    • resource() — fetch genérico con Promise. Estable en v22.
    • rxResource() — puente para servicios basados en Observable. Estable en v22.
    • httpResource() — wrapper declarativo sobre HttpClient. Experimental en v22.

    El matiz de los estados de estabilidad importa. resource() y rxResource() ya son API pública con garantías de compatibilidad. httpResource() sigue marcado como experimental — la API puede cambiar. Para producción crítica, ten eso en cuenta.

    resource(): el punto de entrada

    Usa resource() cuando el origen de datos es una Promise o una función fetch directa. El parámetro reactivo se define en params, y la función que carga los datos en loader.

    import { ChangeDetectionStrategy, Component, signal, resource } from '@angular/core';
    

    interface Producto { id: number; nombre: string; }

    @Component({ selector: 'app-catalogo', changeDetection: ChangeDetectionStrategy.OnPush, template: ` @switch (productos.status()) { @case ('loading') { <p>Cargando...</p> } @case ('reloading') { <p>Actualizando...</p> } @case ('error') { <p>Error: {{ productos.error() }}</p> } @default { @if (productos.hasValue()) { <ul> @for (p of productos.value(); track p.id) { <li>{{ p.nombre }}</li> } </ul> } } } <button (click)="categoria.set(categoria() + 1)">Siguiente</button> <button (click)="productos.reload()">Recargar</button> `, }) export class CatalogoComponent { categoria = signal(1);

    productos = resource<Producto[], { cat: number }>({ params: () => ({ cat: this.categoria() }), loader: ({ params, abortSignal }) => fetch(/api/products?category=${params.cat}, { signal: abortSignal }) .then(r => r.json()), }); }

    Dos errores que verás en código antiguo o en tutoriales desactualizados:

    • El campo se llama params, no request. Eso era la API experimental anterior.
    • Los estados son strings literales: 'loading', 'reloading', 'error'ResourceStatus no es un enum TypeScript nativo — es un objeto de constantes (as const), así que se compara con strings literales, no con ResourceStatus.Loading.

    Cuando categoria cambia, Angular cancela el fetch anterior (usando el abortSignal que recibe el loader) y lanza uno nuevo. El ciclo de vida completo, gestionado sin escribir una sola línea de cleanup.

    rxResource(): para servicios que devuelven Observables

    La mayoría de proyectos Angular tienen servicios basados en HttpClient que devuelven Observable. Migrar todo a fetch puro no es viable ni deseable.

    rxResource() es el puente. En lugar de loader, usa stream, que devuelve un Observable.

    import { ChangeDetectionStrategy, Component, signal, inject } from '@angular/core';
    import { rxResource } from '@angular/core/rxjs-interop';
    import { HttpClient } from '@angular/common/http';
    

    interface Producto { id: number; nombre: string; }

    @Component({ selector: 'app-catalogo-rx', changeDetection: ChangeDetectionStrategy.OnPush, template: ` @if (productos.isLoading()) { <p>Cargando...</p> } @else if (productos.hasValue()) { <ul> @for (p of productos.value(); track p.id) { <li>{{ p.nombre }}</li> } </ul> } `, }) export class CatalogoRxComponent { private http = inject(HttpClient); categoria = signal(1);

    productos = rxResource({ params: () => ({ cat: this.categoria() }), stream: ({ params }) => this.http.get<Producto[]>(/api/products?category=${params.cat}), }); }

    Dos puntos que generan confusión frecuente:

    • El import correcto es @angular/core/rxjs-interop, no @angular/core.
    • El método se llama stream, no loader. Los ejemplos de versiones experimentales anteriores usaban loader, de ahí la confusión.

    httpResource(): la opción declarativa

    httpResource() va un paso más allá: elimina la necesidad de declarar un servicio intermedio para casos de fetching simple. Lo declaras directamente en el componente.

    import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
    import { httpResource } from '@angular/common/http';
    

    interface User { id: number; name: string; }

    @Component({ selector: 'app-user-profile', changeDetection: ChangeDetectionStrategy.OnPush, template: ` @if (user.isLoading()) { <p>Cargando...</p> } @else if (user.error()) { <p>Error al cargar el perfil</p> } @else if (user.hasValue()) { <p>{{ user.value()?.name }}</p> } `, }) export class UserProfileComponent { userId = signal(1);

    user = httpResource<User>(() => /api/user/${this.userId()}); }

    httpResource() expone dos signals exclusivos que no tienen resource() ni rxResource():

    • .statusCode() — el código HTTP de la respuesta (200, 404, 500…)
    • .headers() — las cabeceras de la respuesta

    Ambas son señales independientes de .status(), que sigue siendo el estado del ciclo de vida del recurso. Confundir .status() con .statusCode() es el error más frecuente al empezar con esta API.

    Para respuestas que no son JSON:

    // Texto plano
    const readme = httpResource.text(() => /docs/readme.md);
    

    // Binario const avatar = httpResource.blob(() => /api/user/${this.userId()}/avatar);

    Requiere provideHttpClient() en el bootstrap. Usa HttpClient e interceptores por debajo, así que tus interceptores de autenticación siguen funcionando sin cambiar nada.

    Recuerda que httpResource() sigue marcado como experimental en v22. Para proyectos con requisitos estrictos de estabilidad de API, usa rxResource() con tu servicio HttpClient habitual hasta que se estabilice.

    Cuándo usar cada uno

    No es una decisión complicada si tienes claros los criterios:

    | Situación | API recomendada | |—|—| | Fetch puro o API externa directa | resource() | | Tienes servicios con Observable existentes | rxResource() | | Fetching simple sin servicio intermedio (experimental) | httpResource() | | Necesitas el status code HTTP como signal | httpResource() |

    Las tres APIs comparten la misma superficie de lectura: .value(), .isLoading(), .error(), .hasValue(), .status(), .reload(). Cambiar de una a otra es mínimamente invasivo.

    Validación del dato en runtime

    El genérico de TypeScript solo existe en tiempo de compilación. Si el backend devuelve algo inesperado, httpResource() no lanzará ningún error — simplemente tendrás un objeto mal tipado en runtime.

    La opción parse existe exactamente para este caso:

    import { z } from 'zod';
    

    const UserSchema = z.object({ id: z.number(), name: z.string(), });

    user = httpResource( () => /api/user/${this.userId()}, { parse: UserSchema.parse } );

    Si el dato del backend no cumple el schema, el resource entra en estado 'error' automáticamente. Sin try/catch manual, sin runtime silencioso. Si quieres profundizar en cómo construir schemas robustos con Zod para este tipo de validación, el curso de Zod para TypeScript cubre exactamente estos patrones de producción.

    Lo que cambia en tu arquitectura

    La Resource API no elimina los servicios Angular — los reorganiza. Sigues necesitando servicios para encapsular lógica de negocio compleja, componer múltiples endpoints, o compartir estado entre componentes. Lo que elimina es el boilerplate de gestión de ciclo de vida en los componentes que simplemente cargan y muestran datos.

    Un componente que antes necesitaba un servicio, tres operadores RxJS y un takeUntilDestroyed ahora expresa la misma intención en diez líneas. La lógica no desaparece — se mueve al lugar correcto.

    Si quieres el cuadro completo — Signals, Resource API, Signal Forms, Zoneless y todo lo que llegó en v22 — el curso Angular Moderno tiene el módulo M10 dedicado íntegramente a Resource API con ejemplos sobre el proyecto ShopFlow.

    Qué hacer hoy

    Identifica en tu proyecto los componentes que tienen este patrón: signal o BehaviorSubject como parámetro, switchMap hacia HttpClient, y async pipe en el template.

    Esos son tus candidatos para migrar a rxResource(). No necesitas reescribir los servicios. Solo cambias la forma en que el componente consume el Observable.

    Empieza por un componente de solo lectura — uno que carga datos y no tiene formularios complejos. Comprueba que .status() en el template te da todo lo que necesitabas del loading$ que tenías antes.

    Si funciona ahí, tienes el patrón. El resto de la migración es repetirlo.

    Preguntas frecuentes

    ¿Cuál es la diferencia entre resource() y httpResource() en Angular 22? resource() acepta cualquier función que devuelva una Promise — puedes usarlo con fetch, con SDKs externos, o con cualquier operación asíncrona. httpResource() es un wrapper declarativo sobre HttpClient que además expone el status code HTTP y las cabeceras como signals independientes. La diferencia clave: httpResource() sigue siendo experimental en v22; resource() es API estable.

    ¿Puedo usar rxResource() si tengo servicios que devuelven Observables? Sí. rxResource() está diseñado exactamente para ese caso. En lugar de loader, defines un stream que devuelve un Observable. Tus servicios existentes no cambian — solo cambia cómo el componente los consume.

    ¿La Resource API reemplaza completamente RxJS en Angular? No. RxJS sigue siendo útil para transformaciones complejas de streams, operadores avanzados y casos donde necesitas combinar múltiples fuentes. La Resource API reemplaza el patrón subscribe/unsubscribe para carga de datos HTTP en componentes — no todos los casos de uso reactivo.

    ¿Qué ocurre con los datos en caché cuando cambia el signal de parámetros? Cuando el signal de params cambia, el resource entra en estado 'reloading' (no 'loading'). El valor anterior sigue disponible en .value() durante la recarga. Esto permite mostrar datos obsoletos mientras llegan los nuevos, en lugar de mostrar un spinner que vacía la UI. Es el comportamiento por defecto — no necesitas configurarlo.

    ¿Funciona la Resource API con Angular SSR? Sí. httpResource() usa HttpClient internamente, que ya tiene soporte de transferencia de estado para SSR. Con resource() y rxResource() necesitas gestionar tú mismo la transferencia de estado si el servidor precarga datos. La integración más limpia con SSR actualmente es a través de httpResource() o rxResource() con un servicio que use TransferState.

    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode. Ha migrado proyectos Angular en producción desde v2 hasta v22.

    Sources:

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