Category: Blog

Your blog category

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

  • Angular Signal Forms: por qué reemplaza a Reactive Forms

    Angular Signal Forms: por qué reemplaza a Reactive Forms

    Hace un par de semanas revisé un pull request de un formulario de checkout. Línea 40: const email = form.value.email as string. Le pregunté al autor por qué el cast. Silencio de dos segundos. Luego: "porque si no, TypeScript se queja".

    Ese "se queja" es el síntoma de un problema real que Angular Signal Forms existe justamente para eliminar. En Reactive Forms, form.value.email no es string. Es string | null. Angular lo tipa así porque reset() limpia los valores a null, y el compilador no tiene forma de saber si ya hiciste reset o no — así que cada lectura del formulario viene con un cast disfrazado de costumbre.

    Quince años escribiendo formularios en Angular. Quince años haciendo ese cast sin cuestionarlo. Angular Signal Forms, el nuevo sistema de formularios que llega con Angular v22, es la primera vez que veo una solución que no es un parche — es un cambio de premisa.


    Los 3 problemas reales de Reactive Forms

    Reactive Forms no es "malo" — lleva años en producción y funciona. Pero arrastra tres problemas estructurales que nacen de la misma raíz.

    1. El tipado es una mentira

    Por defecto, cada FormControl tiene tipo T | null, incluso cuando tu modelo de dominio nunca admite null en ese campo.

    form = this.fb.group({
      email: ['', [Validators.required, Validators.email]],
      password: ['', Validators.required],
    });
    // form.value.email es string | null — no string
    

    El tipo del formulario nunca coincide con el tipo del dominio. Terminas casteando con as, o duplicando la interfaz a mano con un NonNullable<T> que mantienes sincronizado manualmente. Ninguna de las dos opciones es tipado real — son formas distintas de callar al compilador.

    2. El formulario es la fuente de verdad, no el modelo

    En Reactive Forms, el estado vive dentro del FormGroup. Tu modelo de dominio es una consecuencia que extraes cuando lo necesitas, no el origen de la verdad.

    Para extraerlo tienes cuatro caminos distintos, todos manuales: patchValue(), getRawValue(), reset() con un objeto de valores, o .get('email')?.setValue(x) campo por campo. Cuatro formas de sincronizar el mismo dato, cada una esperando a que se te olvide usarla en el momento correcto.

    3. ControlValueAccessor es la interfaz más odiada de Angular

    Cualquiera que haya construido un input custom en Angular conoce el ritual: implementar writeValue(), registerOnChange(), registerOnTouched(), setDisabledState(), y registrar un provider NG_VALUE_ACCESSOR en el componente.

    providers: [{
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CustomInputComponent),
      multi: true,
    }]
    

    Son cerca de 20 líneas de boilerplate imperativo por cada control. Sin genéricos reales, sin signals, sin forma de que el compilador te ayude si te equivocas en el tipo del valor que emites.


    Signal Forms: el reinicio conceptual

    Aquí está el punto que más se malinterpreta: Signal Forms no es "Reactive Forms 2". No es una versión mejorada del mismo paradigma con signals encima. Es empezar de cero con una premisa distinta.

    En Reactive Forms, el formulario manda y el modelo es una extracción. En Signal Forms, el modelo es un signal() normal y el formulario es una vista reactiva de ese signal. Cuando el usuario escribe en un input, el signal se actualiza. Cuando tú cambias el signal desde código, el formulario se actualiza solo. Ya no hay cuatro formas de sincronizar — solo tocas el signal.

    Es el mismo giro conceptual que ya vimos con la reactividad fina de signals frente a callbacks y suscripciones manuales: una fuente de verdad única, y todo lo demás reacciona a ella. Signal Forms aplica esa misma idea al dominio de los formularios.

    @Component({
      imports: [FormField, FormRoot],
      template: `
        <form [formRoot]="loginForm">
          <input [formField]="loginForm.email" type="email" />
          @if (loginForm.email().invalid() && loginForm.email().touched()) {
            <span>Email inválido</span>
          }
          <input [formField]="loginForm.password" type="password" />
          <button type="submit">Entrar</button>
        </form>
      `
    })
    export class LoginComponent {
      readonly loginModel = signal({ email: '', password: '' });
      readonly loginForm = form(this.loginModel, loginSchema);
    }
    

    Compara esto con el FormGroup de arriba. No hay fb.group(), no hay Validators.email como clase estática, no hay .get('email'). Hay un signal (loginModel) y una función (form()) que lo envuelve.

    form(), FieldTree y FieldState

    form(model, schema?) recibe tu modelo — y aquí hay una regla estricta: el modelo debe ser un WritableSignal<T>, un signal() normal y escribible. Si le pasas un computed(), no compila. No es un "formulario de solo lectura" — es directamente un error de tipos, porque Signal Forms necesita poder escribir de vuelta en el modelo cuando el usuario interactúa.

    form() devuelve un FieldTree<T>. Accedes a cada campo por dot-notation: loginForm.email es un nodo de ese árbol. Al invocarlo como función — loginForm.email() — obtienes un FieldState, que expone todo como signals: value() (un WritableSignal), dirty(), touched(), invalid(), valid(), errors(), pending(), disabled(), readonly(), required(), hidden(). Y métodos como markAsTouched(), markAsDirty(), reset(). No existe ningún tipo llamado "FieldNode" — es FieldTree para la estructura y FieldState para el estado de un campo concreto.

    Sin módulos, solo directivas standalone

    No existe ningún FormsSignalsModule. Las dos piezas que necesitas son directivas standalone: FormField (aporta [formField], va en cada input) y FormRoot (aporta [formRoot], va en el <form>). Se importan una por una: imports: [FormField, FormRoot].

    Signal Forms vive en @angular/forms/signals, ya incluido si tienes @angular/forms en v21 o superior. No necesitas ningún provider global adicional en app.config.ts — es standalone de principio a fin.

    [formRoot] es la pieza nueva de v21.2/v22 que más simplifica el día a día: aplica novalidate automáticamente, intercepta el submit nativo, hace preventDefault y dispara el envío del FieldTree sin que tengas que escribir un (ngSubmit) manual. El estilo clásico de <form> + (submit) sigue funcionando si lo prefieres, pero el camino recomendado en Signal Forms es el declarativo con [formRoot].

    Esto es exactamente el tipo de arquitectura que estamos actualizando a v22 en el curso de Angular Moderno — el objetivo no es memorizar la API nueva, sino entender por qué el modelo manda ahora en vez del framework.


    Validación con schema declarativo

    La validación se declara aparte, con schema<T>((path) => { ... }). La convención oficial nombra el parámetro path — no f, no form, no otro nombre. Vale la pena respetarla porque es lo que vas a ver en toda la documentación y en el código de otros equipos.

    const loginSchema = schema<LoginForm>((path) => {
      required(path.email);
      email(path.email);
      required(path.password);
      minLength(path.password, 8);
    });
    

    Los validadores built-in siguen todos el mismo patrón, con el path primero: required(path.campo), email(path.campo), minLength(path.campo, n), maxLength(path.campo, n), min(path.campo, n), max(path.campo, n), pattern(path.campo, regex).

    Si escribes un validador custom, hay una regla que no es opcional: debe devolver undefined en caso de éxito, nunca null. Es la "regla del undefined" — un detalle pequeño que rompe la validación entera si lo pasas por alto viniendo de la costumbre de Reactive Forms, donde null era la señal de "todo bien".


    Controles custom sin ControlValueAccessor

    Los controles custom en Signal Forms se implementan con la interfaz FormValueControl<T>, que reemplaza a ControlValueAccessor con un solo miembro obligatorio: value, como ModelSignal<T> creado con model(). Todo lo demás — disabled, errors, touched, required — son input() opcionales.

    Es aquí donde Signal Forms cambia más el día a día si construyes design systems o librerías de componentes internas. ControlValueAccessor exige implementar cuatro métodos y registrar un provider, como vimos arriba.

    export class CustomInputComponent implements FormValueControl<string> {
      readonly value = model('');                      // único miembro REQUERIDO
      readonly disabled = input(false);                 // opcional
      readonly errors = input<ValidationError[]>([]);   // opcional
    }
    

    Eso es todo. Sin forwardRef, sin NG_VALUE_ACCESSOR, sin registerOnChange guardando una función en una propiedad privada. El compilador conoce el tipo real del valor porque model<string>() lo declara, no porque tú lo prometas en un comentario.


    El estado real de madurez en v22 — sin exagerar

    Signal Forms es @experimental desde Angular v21. En v22, la API core madura significativamente — el comportamiento de form(), [formField], schema() y los validadores built-in se estabiliza. Pero el marcado @experimental puede seguir presente en algunos subconjuntos de la API. Funciones más avanzadas, como validateAsync() o compatForm(), pueden tener cambios menores antes de la estabilización oficial completa.

    Este es exactamente el tipo de dato que el ecosistema Angular audita de cerca, así que voy a ser preciso: "ya es estable, úsalo sin pensar" sería una simplificación que no te sirve para decidir en producción. Puedes verificar el estado exacto de cada API en la documentación oficial de formularios de Angular.

    En la práctica, esto se traduce en tres escenarios:

    Escenario Recomendación
    Código nuevo en tu proyecto propio o side project Úsalo sin reservas
    Código nuevo en un proyecto de empresa Evalúa el riesgo, documenta la decisión con el equipo
    Migrar formularios legacy críticos Espera a la estabilización completa

    Si decides adoptarlo ahora, blindarlo con tests importa más que nunca — y el cambio de paradigma en testing es tan grande como en los formularios mismos. Testear un FieldTree no es testear un FormGroup: lees el value() directamente como signal, sin suscribirte a valueChanges ni esperar un ciclo extra de detección de cambios para que el observable propague. Es exactamente el tipo de ajuste que cubrimos en el curso de Testing en Angular actualizado a este modelo.


    Reactive Forms vs Signal Forms — comparativa

    Reactive Forms Signal Forms
    Tipado T | null por defecto, casts frecuentes El tipo del modelo es el tipo real, sin null fantasma
    Fuente de verdad El FormGroup — el modelo se extrae El signal() del modelo — el form es una vista
    Controles custom ControlValueAccessor, ~20 líneas, sin genéricos FormValueControl<T>, solo value requerido
    Boilerplate Alto: provider, 4 métodos, forwardRef Bajo: directivas standalone, sin módulo
    Estado de madurez (v22) Estable, años en producción @experimental, API core estable en comportamiento

    La tesis

    Signal Forms no es una feature más de la lista de novedades de v22. Es la misma premisa que ya cambió cómo pensamos la reactividad con signal(), computed() y effect(), aplicada ahora al dominio de los formularios: el modelo manda, el framework refleja.

    Angular lleva desde v16 moviéndose hacia signals-first, y Signal Forms es la pieza que faltaba para que esa filosofía cubriera también la parte más tediosa de cualquier aplicación real. Si quieres ver dónde encaja dentro del resto de cambios de esta versión, lo cubrimos en el repaso de novedades de Angular v22.

    Lo que puedes hacer hoy: si tienes un side project o un proyecto nuevo sin presión de legacy, monta el próximo formulario con Signal Forms. No esperes a que el @experimental desaparezca del todo — la API core ya se comporta como se va a comportar. Si estás en un proyecto de empresa, documenta la decisión y evalúa el riesgo con tu equipo antes de migrar nada crítico.

    En Dominicode Labs estamos ya construyendo con Signal Forms en los proyectos de la comunidad — si quieres ver los patrones reales, sin el filtro del ejemplo de documentación, es ahí donde está pasando.


    Preguntas frecuentes sobre Angular Signal Forms

    ¿Qué es Angular Signal Forms?

    Es el nuevo sistema de formularios de Angular, disponible desde v21 como @experimental y madurando en v22. En vez de que el formulario sea la fuente de verdad, el modelo es un signal() normal y el formulario es una vista reactiva de ese signal. Se construye con form(model, schema), que devuelve un FieldTree, y se conecta al template con las directivas standalone [formField] y [formRoot].

    ¿Signal Forms reemplaza a Reactive Forms?

    Conceptualmente sí, pero no de un día para otro. Reactive Forms sigue siendo estable y soportado. Signal Forms no es "Reactive Forms con signals encima" — es un sistema construido desde cero sobre la premisa de que el modelo manda. Para proyectos nuevos, es la dirección a seguir. Para formularios existentes en producción, migrarlos no es urgente todavía.

    ¿Puedo usar Signal Forms en producción en Angular 22?

    Depende del contexto. En tu proyecto propio, sí, sin reservas — la API core (form(), [formField], schema(), validadores built-in) se estabiliza en comportamiento en v22. En un proyecto de empresa, evalúa el riesgo y documenta la decisión, porque el marcado @experimental puede seguir en algunos subconjuntos de la API. Para migrar formularios legacy críticos, espera a la estabilización oficial completa.

    ¿Necesito importar un módulo para usar Signal Forms?

    No. No existe ningún FormsSignalsModule. Las directivas FormField y FormRoot son standalone y se importan directamente en el array imports del componente. Tampoco necesitas ningún provider adicional en app.config.ts — solo tener @angular/forms en v21 o superior.

    ¿Cómo se hacen controles de formulario custom en Signal Forms?

    Con la interfaz FormValueControl<T>, que reemplaza a ControlValueAccessor. Solo necesitas un miembro obligatorio: value como ModelSignal<T> creado con model(). Propiedades como disabled, errors, touched o required son input() opcionales, sin necesidad de provider ni forwardRef.


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

  • Claude Code hooks: guardrails, logging y automatización para tus agentes

    Claude Code hooks: guardrails, logging y automatización para tus agentes

    Hook PreToolUse para Bash: bloquea rm -rf y loguea todo

    set -euo pipefail

    Leer el JSON de entrada desde stdin

    INPUT=$(cat)

    Extraer el comando que Claude quiere ejecutar

    COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')

    Timestamp para el log

    TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
    LOG_FILE="${CLAUDE_PROJECT_DIR:-$HOME}/.claude/bash-audit.log"

    Loguear el comando (siempre, antes de cualquier decisión)

    echo "[$TIMESTAMP] CMD: $COMMAND" >> "$LOG_FILE"

    Patrones peligrosos que bloqueamos sin excepciones

    BLOCKED_PATTERNS=(
    "rm -rf /"
    "rm -rf ~"
    "rm -rf *"
    "rm -rf ."
    ":(){ :|:& };:"
    "dd if=/dev/zero"
    "> /dev/sda"
    "mkfs."
    )

    for PATTERN in "${BLOCKED_PATTERNS[@]}"; do
    if echo "$COMMAND" | grep -qE "$PATTERN"; then
    echo "[$TIMESTAMP] BLOCKED: $COMMAND" >> "$LOG_FILE"
    echo "Comando bloqueado por hook de seguridad: patrón destructivo detectado ('$PATTERN')" >&2
    exit 2
    fi
    done

    Todo bien — salida silenciosa, flujo normal

    exit 0

    
    Ahora la configuración en `.claude/settings.json`:
    
    ```json
    {
      "hooks": {
        "PreToolUse": [
          {
            "matcher": "Bash",
            "hooks": [
              {
                "type": "command",
                "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/bash-guard.sh",
                "timeout": 10
              }
            ]
          }
        ]
      }
    }
    

    Dale permisos de ejecución al script:

    chmod +x .claude/hooks/bash-guard.sh
    

    A partir de aquí, cada vez que Claude intente ejecutar un comando Bash, el hook se dispara primero. Si detecta un patrón peligroso, Claude recibe el mensaje de error en stderr y no ejecuta nada. Si todo está limpio, el agente continúa sin ninguna interrupción visible.

    El archivo bash-audit.log crece con cada comando ejecutado. En una sesión de trabajo normal con un agente activo, ese log te cuenta la historia completa de lo que hizo Claude — sin tener que scrollear el historial de conversación.


    Añadir una notificación cuando el agente termina

    Si lanzas tareas largas y quieres saber cuándo terminan sin estar mirando la pantalla, el hook Stop es lo que necesitas.

    {
      "hooks": {
        "Stop": [
          {
            "hooks": [
              {
                "type": "command",
                "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/notify-done.sh",
                "timeout": 5
              }
            ]
          }
        ]
      }
    }
    
    #!/bin/bash
    # .claude/hooks/notify-done.sh
    # Notificación de escritorio cuando Claude termina una tarea
    
    # En macOS
    if command -v osascript &> /dev/null; then
      osascript -e 'display notification "Claude ha terminado la tarea" with title "Claude Code"'
    fi
    
    # En Linux con notify-send
    if command -v notify-send &> /dev/null; then
      notify-send "Claude Code" "El agente ha terminado la tarea"
    fi
    
    exit 0
    

    El hook Stop no tiene matcher porque no hay herramientas que filtrar — aplica siempre que Claude decide parar. Si necesitas que Claude continúe trabajando hasta que se cumpla alguna condición (por ejemplo, todos los tests en verde), haz que el script devuelva exit 2 y escribe en stdout un JSON con {"hookSpecificOutput": {"additionalContext": "Los tests aún fallan. Corrígelos antes de terminar."}} para que Claude sepa qué debe hacer a continuación. El stderr en Stop hooks no interrumpe el flujo.


    Cuándo usar hooks, cuándo CLAUDE.md y cuándo sub-agentes

    Esta es la pregunta que más se repite cuando alguien empieza a añadir capas de control a sus agentes.

    Usa CLAUDE.md para instrucciones de comportamiento en lenguaje natural: convenciones de código, qué herramientas preferir, cómo formatear los commits. Es lo primero que Claude lee. Es contexto, no control.

    Usa hooks cuando necesitas una garantía técnica que no dependa de que Claude interprete bien una instrucción. Un rm -rf bloqueado por un hook es un rm -rf bloqueado, siempre, independientemente de cómo estaba redactado el prompt. Un rm -rf "prohibido" en CLAUDE.md es una sugerencia que Claude puede ignorar bajo presión de contexto.

    Usa sub-agentes cuando necesitas razonamiento sobre una situación: revisar si el código generado cumple los requisitos de arquitectura, validar que una migración de base de datos es correcta antes de ejecutarla, resumir los resultados de diez herramientas en paralelo. Los sub-agentes piensan. Los hooks no necesitan pensar — esa es su ventaja.

    La regla general: hooks para lo que debe ser determinista, sub-agentes para lo que requiere juicio.


    Preguntas frecuentes

    ¿Los hooks se ejecutan con cada mensaje del usuario o solo cuando Claude usa herramientas?

    Depende del tipo de hook. PreToolUse y PostToolUse solo se disparan cuando Claude invoca una herramienta — no con cada mensaje de texto. UserPromptSubmit se dispara con cada mensaje enviado, antes de que Claude lo procese. Stop se dispara cuando Claude decide terminar, no cuando el usuario escribe algo.

    ¿Puedo tener hooks diferentes para proyectos distintos?

    Sí. Los hooks en .claude/settings.json (dentro del proyecto) solo aplican a ese proyecto. Los hooks en ~/.claude/settings.json aplican a todos tus proyectos. Si hay configuraciones en ambos archivos, se combinan. En caso de conflicto en el mismo evento, la configuración más específica (proyecto) tiene precedencia.

    ¿Un hook puede modificar lo que Claude va a hacer, no solo bloquearlo?

    Sí, en PreToolUse. Puedes devolver por stdout un JSON con hookSpecificOutput.updatedInput para reemplazar los argumentos que Claude iba a usar. Por ejemplo, si Claude quiere ejecutar rm -rf build, puedes interceptarlo y devolver rm -rf build/ (con trailing slash) para que solo borre el contenido del directorio, no el directorio en sí. Esta capacidad es poderosa — úsala con cuidado.

    ¿Hay alguna forma de ver qué hooks están activos en mi sesión?

    Sí. Escribe /hooks en el prompt de Claude Code y se abre una vista en el navegador con todos los hooks configurados, organizados por evento, con su matcher y tipo de handler. Es de solo lectura, pero es la forma más rápida de auditar qué está activo.

    ¿Los hooks se pueden desactivar sin borrarlos?

    Sí. Añade "disableAllHooks": true en cualquiera de los archivos de settings. Solo los settings de usuario y proyecto pueden desactivar hooks definidos en esos mismos niveles — los hooks de configuración administrada (managed settings) requieren intervención del administrador.

    ¿Hay límite en cuántos hooks puedo configurar?

    No hay un límite documentado en el número de hooks. Sí hay un timeout por hook (por defecto 600 segundos para comandos, 30 para prompts). Si un hook supera el timeout, se cancela como error no bloqueante (igual que un exit 1) — el flujo continúa pero el hook no tuvo efecto.


    Lo que cambia cuando añades hooks a tu workflow

    La primera semana que empecé a usar hooks en mis propios agentes, lo que más me sorprendió no fue la seguridad — fue la visibilidad.

    El archivo de log de comandos Bash me reveló patrones que no había visto antes. Claude ejecutaba con frecuencia ciertos comandos que yo no esperaba. Algunos eran ineficientes. Uno de ellos era potencialmente problemático en un contexto de CI. Sin el log, nunca me habría enterado.

    Los hooks no solo protegen tu sistema. Te dan información real sobre cómo trabaja el agente — y esa información es la que necesitas para mejorar tus prompts, tu CLAUDE.md y tu arquitectura de agentes con el tiempo.

    Si estás construyendo algo serio con Claude Code — más de un agente, un workflow automatizado, código que toca producción —, los hooks no son opcionales. Son la diferencia entre un agente que funciona y uno en el que confías.

    Si quieres ver cómo encajan los hooks dentro de un sistema de agentes más completo — con sub-agentes, routines y MCP — en el curso Construye con IA cubrimos el stack completo desde la idea hasta el producto, incluyendo cómo estructurar los guardrails de seguridad para workflows que corren sin supervisión constante.

    Y si prefieres un entorno donde experimentar con otros developers que están construyendo lo mismo, en Dominicode Labs compartimos proyectos, configuraciones y workflows reales cada semana.


    Bezael Pérez — Developer senior, fundador de Dominicode. Lleva 15+ años construyendo software y los últimos años construyendo con IA. Escribe sobre arquitectura de agentes, Angular moderno y cómo pasar de idea a producto sin caos.

  • MCP Server en TypeScript: conecta Claude Code con cualquier API

    MCP Server en TypeScript: conecta Claude Code con cualquier API

    claude mcp add –transport stdio github-issues — node /ruta/absoluta/build/index.js

    Para todos los proyectos (ámbito global del usuario)

    claude mcp add –scope user –transport stdio github-issues — node /ruta/absoluta/build/index.js

    
    Verifica que Claude Code lo reconoce:
    
    ```bash
    claude mcp list
    

    Deberías ver github-issues en el listado con estado Pending approval. Una vez que lo apruebes desde Claude Code, pasará a connected.


    Cómo probarlo desde una sesión de Claude Code

    Abre Claude Code en cualquier directorio y escribe:

    Lista los issues abiertos del repo microsoft/vscode
    

    Claude detecta que tiene acceso al tool list_issues, lo llama con { owner: "microsoft", repo: "vscode", state: "open" }, y devuelve la lista formateada directamente en el chat.

    Sin salir. Sin copiar y pegar. Sin fricción.

    Para repos privados, añade tu token de GitHub como variable de entorno antes de registrar el server:

    # En el comando de registro pasa el env directamente
    claude mcp add --transport stdio github-issues --env GITHUB_TOKEN=ghp_xxx -- node /ruta/absoluta/build/index.js
    

    Y en el código, descomenta la línea Authorization: Bearer ${process.env.GITHUB_TOKEN}.


    Ir más allá: cuándo crear tu propio MCP server

    Esta es la pregunta real. El ecosistema de MCP servers públicos ya tiene integraciones para GitHub, Slack, Notion, bases de datos, filesystems, y decenas más. No construyas lo que ya existe.

    Crea tu propio server cuando:

    1. Tienes una API interna que nadie más va a integrar
    2. Necesitas transformar o filtrar datos antes de que lleguen al modelo — la lógica de negocio importa
    3. Quieres controlar exactamente qué puede hacer Claude y qué no en tu entorno
    4. Estás construyendo un producto y necesitas que Claude interactúe con él de forma programática

    El patrón que acabas de aprender escala sin cambios. Añadir un tool nuevo es copiar el bloque del handler y registrarlo en ListToolsRequestSchema. Añadir autenticación es una cabecera. Añadir caché es un Map en memoria.

    El scaffold es siempre el mismo. Lo que cambia es la lógica de negocio de cada tool.

    Si quieres profundizar en este modelo de trabajo — construir con IA de forma estructurada, con specs, con MCP servers propios, con agentes que hacen trabajo real — en el curso Construye con IA: De la Idea al Producto con Claude Code trabajamos exactamente este flujo. Desde la idea hasta tener algo en producción.


    FAQ

    ¿Necesito compilar TypeScript para usar el server? ¿No puedo usar tsx directamente?

    Puedes. Para desarrollo local, tsx src/index.ts funciona. Para registrar en Claude Code de forma estable, compilar a JS es más fiable porque no dependes de que tsx esté instalado globalmente. En el comando claude mcp add puedes usar npx tsx si prefieres:

    claude mcp add --transport stdio github-issues -- npx tsx /ruta/src/index.ts
    

    ¿Cuál es la diferencia entre stdio y HTTP como transporte?

    StdioServerTransport es el modo local: Claude Code lanza tu server como proceso hijo y se comunica por stdin/stdout. Es el modo más simple y suficiente para tools personales o de equipo. El transporte HTTP (Streamable HTTP) es para servers remotos que quieres exponer como servicio — por ejemplo, si construyes un MCP server para tu empresa y lo despliegas en un servidor.

    ¿Mis tools pueden leer archivos del sistema o ejecutar comandos?

    Sí, un MCP server tiene acceso completo al sistema donde se ejecuta. Puede leer archivos con fs, ejecutar procesos con child_process, hacer peticiones de red. Eso también es la responsabilidad: el server corre con los permisos del usuario que lo lanza, así que diseña los tools con cuidado y no expongas capacidades destructivas sin confirmación.

    ¿Funciona con Claude Desktop o solo con Claude Code?

    Funciona con cualquier cliente MCP compatible. Claude Desktop usa claude_desktop_config.json en lugar de claude mcp add, pero el server es exactamente el mismo. También es compatible con Cursor, Continue, y cualquier cliente que implemente el protocolo. Ese es el punto de MCP: escribes el server una vez, lo consumes desde donde quieras.

    ¿Puedo añadir varios tools al mismo server?

    Sí, y es lo recomendable cuando los tools comparten contexto. Un server de GitHub podría tener list_issues, create_issue, list_pull_requests y get_file_content en el mismo proceso. Cada tool se declara en el handler de ListToolsRequestSchema y se implementa en el bloque if correspondiente dentro de CallToolRequestSchema.


    Conclusión

    Ya sabes cómo funciona MCP, qué son los tres primitivos, y tienes un server real funcionando que conecta Claude Code con la API de GitHub. El siguiente paso es obvio: sustituye la llamada a GitHub por la API que necesites tú.

    Si estás construyendo flujos de trabajo con agentes IA y quieres ir más allá de los MCP servers públicos, en Dominicode Labs publicamos proyectos completos, code reviews y recursos exclusivos para developers que construyen con IA en serio.

    Para entender cómo Claude Code orquesta tools, sub-agentes y contexto dentro de una sesión, lee primero la introducción a Claude Code que publiqué aquí — es el punto de entrada que te va a dar el marco conceptual completo.


    Bezael Pérez — Developer senior, fundador de Dominicode. 15+ años construyendo software. Ahora construyendo con IA.

  • CLAUDE.md y memoria persistente: mi flujo real con Claude Code

    CLAUDE.md y memoria persistente: mi flujo real con Claude Code

    Nombre y propósito del proyecto

    [Una o dos líneas. Para qué sirve y quién lo opera.]

    Reglas globales

    [Idioma, tono, convenciones no negociables. Las cosas que si Claude Code
    ignora, el output es inutilizable.]

    Estructura del repositorio

    [Árbol de directorios con una línea explicando qué hay en cada carpeta.
    Claude Code necesita saber dónde está cada cosa sin tener que explorar.]

    Comandos disponibles

    [Los scripts, CLIs y comandos que puede ejecutar. Con ejemplo real de uso.]

    Convenciones de nomenclatura

    [Patrones de nombres de archivos. Crítico para proyectos con muchos docs.]

    Qué NO hacer

    [Igual de importante que lo que sí hacer. Archivos que no tocar,
    patrones que evitar, decisiones ya tomadas que no reabrir.]

    
    Lo que no incluyo: historia del proyecto, motivaciones, "por qué elegimos X tecnología". Eso es contenido para un ADR o el README. El CLAUDE.md tiene que ser operativo al 100%.
    
    **Longitud objetivo: menos de 200 líneas.** Si supera eso, estás incluyendo demasiado. Claude Code no necesita el contexto completo de cada decisión — necesita las reglas de operación.
    
    ### Lo que la mayoría mete en CLAUDE.md y no debería
    
    He revisado muchos CLAUDE.md de proyectos de developers en la comunidad. El error más común: meter todo lo que "podría ser útil".
    
    Eso mata el propósito del documento. Cuando el CLAUDE.md tiene 500 líneas, Claude Code lo lee entero pero no distingue qué es crítico y qué es relleno. El resultado es el mismo que no tener CLAUDE.md: ruido.
    
    Solo va al CLAUDE.md lo que, si Claude Code lo ignora, rompe el proyecto o produce output inutilizable.
    
    ---
    
    ## El sistema de memoria persistente
    
    El contexto de una sesión de Claude Code desaparece cuando la sesión termina. Eso es una limitación real y no va a cambiar pronto — la ventana de contexto no es memoria a largo plazo.
    
    El workaround que funciona: archivos Markdown.
    
    ### La estructura que uso
    
    En el directorio del proyecto tengo una carpeta `memory/` con dos tipos de archivos:
    
    1. **`MEMORY.md`** — el índice. Una lista de una línea por cada archivo de memoria con un enlace y una descripción de qué contiene. Claude Code lo lee al arrancar la sesión y sabe qué hay disponible.
    
    2. **Archivos individuales de memoria** — uno por tema. Nomenclatura descriptiva: `project_kursar.md`, `feedback_email_style.md`, `reference_tools.md`.
    
    Una entrada en `MEMORY.md` tiene esta forma:
    
    ```markdown
    # Memory Index — Dominicode Company Agents
    
    - [User Profile](user_profile.md) — Solo creator, YouTube + Udemy + books, comunidad en español
    - [Curso Angular 22](project_curso_angular22.md) — Regrabación en curso; ejemplos verificados en ejemplos/v22-features/
    - [Estilo emails Bezael](feedback_email_style.md) — Abrir con historia breve; no estilo telegráfico
    - [WordPress taxonomía](reference_wordpress_taxonomia.md) — IDs reales verificados (AI=37, TypeScript=42…)
    

    Hay tres prefijos que uso para distinguir el tipo de contenido:

    • project_ — estado de un proyecto activo con decisiones tomadas
    • feedback_ — algo que salió mal o que aprendí de una sesión anterior y no quiero volver a repetir
    • reference_ — datos estáticos que Claude Code necesita consultar (IDs, URLs, credenciales de formato)

    Por qué funciona mejor que repetirlo en cada sesión

    La alternativa es pegar el contexto en el primer prompt de cada sesión. Lo hice durante semanas. El problema: acumulas un primer prompt de 800 palabras que tarde o temprano omites porque es tedioso, y cuando lo omites, Claude Code trabaja sin ese contexto.

    Con archivos de memoria, el contexto está disponible siempre que Claude Code los lea. Y como están versionados en el repo, no se pierden entre sesiones ni entre máquinas.

    El inconveniente honesto: Claude Code no lee esos archivos automáticamente a menos que se lo indiques. Tienes que incluirlos en el arranque de sesión o referenciarlos con @archivo cuando son relevantes. Esto lo resuelvo con el ritual de inicio que cuento más adelante.


    Gestión del contexto en sesiones largas

    Esto es lo que menos se habla y lo que más impacta en la calidad del trabajo.

    Una sesión larga de Claude Code acumula contexto de forma lineal. Cada intercambio, cada archivo leído, cada respuesta generada ocupa espacio en la ventana. Cuando la ventana se llena, el modelo empieza a "comprimir" el historial — mantiene las instrucciones recientes y los bloques de código más relevantes, pero los matices de conversaciones anteriores se difuminan.

    El resultado es exactamente lo que me pasó esa tarde: Claude Code responde con coherencia local (el último intercambio está bien) pero pierde coherencia global (contradice decisiones tomadas hace cuarenta minutos).

    Cómo lo detecto

    Hay tres señales de que el contexto está degradado:

    • Claude Code propone algo que ya descartamos explícitamente en la misma sesión
    • Las respuestas se vuelven más genéricas y pierden el tono específico del proyecto
    • Me pide información que ya le di al inicio de la sesión

    Cuando aparece cualquiera de las tres, no sigo. Empiezo sesión nueva.

    Cuándo empezar sesión nueva (aunque duela)

    La respuesta rápida: cuando terminas un bloque de trabajo concreto.

    No esperes a que el contexto se degrade. Trata cada sesión de Claude Code como una unidad de trabajo enfocada. Si estoy escribiendo un post del blog, esa es la sesión. Si paso a revisar el curriculum de un curso, es una sesión nueva.

    Este cambio de mentalidad es lo que más impacta en la consistencia del output. Una sesión larga y dispersa produce resultados mediocres. Sesiones cortas y enfocadas producen resultados que puedes usar directamente.

    @files: cuándo y cómo los uso

    Claude Code tiene la sintaxis @archivo para incluir el contenido de un archivo específico en el contexto. Es la herramienta más infrautilizada que conozco entre developers que llevan meses con Claude Code.

    Uso @archivo para tres cosas:

    Dar contexto específico sin abrir un archivo manualmente. Si estoy trabajando en el agente de blog y necesito que Claude Code vea el estado actual del MEMORY.md, escribo @memory/MEMORY.md en el prompt. El contenido entra directamente en el contexto sin que yo tenga que copiarlo.

    Anclar decisiones pasadas. Si en una sesión nueva necesito que recuerde una decisión de arquitectura que está en specs/agentkit-pro/spec.md, la referencio con @. Entra en el contexto de esa sesión específicamente donde la necesito.

    Forzar coherencia entre archivos. Si estoy modificando un componente y quiero que Claude Code sea consciente de cómo lo usa otro módulo, incluyo ambos con @. Sin eso, trabaja con el archivo aislado y puede romper la integración.

    Lo que no hago: incluir diez archivos con @ en el mismo prompt. Cuantos más archivos incluyes, más contexto consumes antes de empezar el trabajo real. Selecciono solo los que son directamente relevantes para la tarea concreta de esa sesión.


    El ritual de inicio de sesión

    Después de meses ajustando esto, tengo un primer prompt que uso como plantilla base. No es magia — es contexto específico entregado de forma eficiente.

    Contexto de esta sesión:
    - Proyecto: [nombre]
    - Tarea: [qué voy a hacer hoy, en una línea]
    - Decisiones previas que aplican: @memory/MEMORY.md
    - Archivos relevantes: @[archivo-1] @[archivo-2]
    - Restricciones: [lo que NO quiero que haga en esta sesión]
    
    Empieza por [primera acción concreta].
    

    Los tres elementos críticos son:

    La tarea en una línea. No el proyecto entero, solo lo que hacemos hoy. Cuanto más específico, mejor el foco de Claude Code durante toda la sesión.

    Las restricciones. Es lo que más me ha ahorrado tiempo. "No toques el archivo X", "no propongas cambiar el stack", "si necesitas más información, pregunta antes de generar código". Sin restricciones explícitas, Claude Code optimiza para completar la tarea con las decisiones que considera mejores — que no siempre son las que tú ya tomaste.

    Una primera acción concreta. No "ayúdame con el proyecto". Sino "lee el archivo X y dime si la estructura de directorios es coherente con las reglas de CLAUDE.md". La primera acción específica establece el tono de toda la sesión.


    Lo que todavía falla y cómo lo mitigo

    Honestidad completa aquí, porque la mayoría de posts sobre Claude Code solo muestran los casos de éxito.

    Los archivos de memoria no se actualizan solos. Si en una sesión tomo una decisión importante — por ejemplo, cambio la arquitectura de un módulo o descubro que una librería no funciona para mi caso de uso — tengo que acordarme de actualizar el archivo de memoria correspondiente antes de cerrar la sesión. Si no lo hago, en la siguiente sesión Claude Code no tiene ese contexto. Todavía me olvido. La solución parcial: incluir "actualiza MEMORY.md con las decisiones de esta sesión" como último paso de cada sesión de trabajo.

    El CLAUDE.md global a veces entra en conflicto con el del proyecto. Tengo reglas globales que son sensatas para el 90% de mis proyectos pero que en algún proyecto específico quiero anular. Claude Code no siempre resuelve bien ese conflicto — a veces aplica la regla global aunque el CLAUDE.md del proyecto diga lo contrario. La solución: en el CLAUDE.md del proyecto, cuando necesito anular una regla global, lo digo explícitamente: "Aunque el CLAUDE.md global indica X, en este proyecto aplicamos Y."

    La compresión de contexto no es predecible. No hay un indicador que te diga "estás al 80% de la ventana de contexto, es hora de empezar sesión nueva". Lo detecto por los síntomas que describí antes. Estoy esperando que Claude Code añada algún tipo de indicador de uso de contexto — de momento no existe.

    Las sesiones cortas y enfocadas son más difíciles de mantener. Cuando estoy en el flow, la tentación de seguir en la misma sesión es real. Cada vez que cedo, la calidad del output en la segunda mitad de la sesión baja. Es un problema de disciplina, no de herramienta.


    FAQ

    ¿Cuántas secciones debe tener un CLAUDE.md?

    No hay un número correcto. Lo importante es que cada sección tenga una función operativa clara. Si no puedes responder "qué hace Claude Code diferente por tener esta sección", esa sección sobra. En mis proyectos suelo tener entre 5 y 8 secciones.

    ¿Puedo tener múltiples CLAUDE.md en subdirectorios?

    Sí. Claude Code lee el CLAUDE.md del directorio raíz y también los de subdirectorios cuando trabaja en ellos. Esto es útil en monorepos o cuando tienes un frontend y un backend con convenciones distintas. No lo abuses — si tienes CLAUDE.md en diez subdirectorios, el agente pasa más tiempo leyendo instrucciones que trabajando.

    ¿Qué diferencia hay entre poner algo en CLAUDE.md y decirlo en el primer prompt?

    El CLAUDE.md aplica a todas las sesiones del proyecto de forma permanente. El primer prompt aplica solo a esa sesión. Usa CLAUDE.md para convenciones estables que no cambian entre sesiones. Usa el primer prompt para el contexto específico de lo que haces hoy.

    ¿Cuándo tiene sentido usar memoria persistente vs. simplemente tener un CLAUDE.md más completo?

    CLAUDE.md es para reglas e instrucciones: cómo trabajar en este proyecto. Los archivos de memoria son para estado e historial: qué ha pasado ya, qué decisiones están tomadas, qué feedback recibí en sesiones anteriores. Si en tu CLAUDE.md estás escribiendo cosas como "el curso de Angular lleva dos semanas atrasado" o "el cliente pidió cambiar el color primario a azul", eso debería ir en un archivo de memoria, no en CLAUDE.md.

    ¿Funciona igual en proyectos de código que en proyectos de contenido?

    Igual de bien, o incluso mejor en proyectos de contenido. Todo lo que describí aquí lo uso tanto para el repositorio de código de Kursar como para el sistema de agentes de Dominicode — que no tiene una sola línea de código productivo, pero tiene 18 agentes, 118 documentos en la base de conocimiento, y decisiones editoriales acumuladas durante meses. El sistema de memoria persistente es especialmente valioso cuando el "código" son documentos, estrategias y decisiones.


    Conclusión

    El contexto no es un detalle técnico de Claude Code que puedas ignorar. Es el recurso central que determina si el agente trabaja contigo o contra ti.

    CLAUDE.md bien estructurado te da coherencia por defecto. La memoria persistente te da continuidad entre sesiones. El ritual de inicio te da foco en cada sesión. Y saber cuándo empezar sesión nueva te salva de la degradación silenciosa que destruye la calidad del output.

    No necesitas implementar todo esto de golpe. Empieza por el CLAUDE.md del proyecto — 100 líneas operativas, sin relleno. Eso solo ya cambia radicalmente cómo trabaja Claude Code en tu repositorio.

    Si quieres ver este sistema aplicado a un proyecto real de principio a fin, en el curso Construye con IA trabajamos exactamente con este flujo: CLAUDE.md, memoria, gestión del contexto y SDD como metodología para que el agente tenga siempre el contexto correcto en el momento correcto.

    Y si ya tienes Claude Code corriendo y quieres profundizar con otros developers que están en el mismo camino, en Dominicode Labs compartimos los patrones que van funcionando en producción — incluyendo los que fallan y cómo los arreglamos.


    Posts relacionados


    Bezael Pérez es developer senior con 15+ años de experiencia y fundador de Dominicode. Construye con Claude Code, Angular y TypeScript, y documenta lo que funciona — y lo que no — para developers que quieren ir más allá del vibe coding.

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

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

    ANTHROPIC_API_KEY=sk-ant-xxxxxxxx

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

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


    La estructura del AiModule

    Antes de escribir código, la estructura:

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

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


    Paso 1: El DTO de validación

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

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

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

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

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

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


    Paso 2: El AiService

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

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

    Dos decisiones importantes aquí:

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

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

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


    Paso 3: El AiController con streaming

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

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

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

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

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


    Paso 4: El AiModule

    El módulo agrupa las tres piezas:

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

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


    Rate limiting: el paso que nadie incluye

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

    npm install @nestjs/throttler
    

    Configúralo en AppModule:

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

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

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


    Conectar con el frontend Angular

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

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

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

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

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

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

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

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

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

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

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

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


    Ejecutar el servidor

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

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

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

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


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

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

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

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

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

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

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


    FAQ

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

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

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

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

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

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

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

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

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

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

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

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


    Cierre

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

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

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

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


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

  • Prompt Caching en Claude: reduce tu factura de API un 90%

    Prompt Caching en Claude: reduce tu factura de API un 90%

    El mes pasado revisé los gastos de API de un proyecto que lleva seis semanas en producción. Un agente conversacional para análisis de documentos legales. El cliente lo usa unas 40 veces al día.

    La factura: $340 en un mes.

    El system prompt tenía 8.000 tokens. Las definiciones de herramientas, otros 3.000. En cada llamada, esos 11.000 tokens se procesaban desde cero. Cuarenta veces al día. Treinta días al mes.

    Activé prompt caching. La siguiente factura: $38.

    No cambié la lógica del agente. No modifiqué los prompts. Solo añadí tres líneas de configuración.

    Eso es lo que hace el prompt caching de Claude. Y la mayoría de developers que trabajan con la API de Anthropic aún no lo tienen activado.


    Qué es el prompt caching y cómo funciona

    Cuando haces una llamada a la API de Claude, pagas por cada token que el modelo procesa. System prompt, herramientas, historial de conversación, contexto de documentos: todo se cobra como tokens de entrada.

    El problema es que en la mayoría de aplicaciones reales, una parte enorme de esos tokens es idéntica en cada llamada. Tu system prompt no cambia. Las definiciones de tus herramientas no cambian. El contexto de un documento que estás analizando no cambia entre preguntas del usuario.

    El prompt caching te permite marcar esas partes estáticas para que Claude las almacene en caché. La documentación oficial de prompt caching cubre todos los modelos y casos edge. La primera vez que se procesa ese contenido, se escribe en caché. En las llamadas posteriores, en lugar de reprocesar esos tokens, Claude los lee desde el caché.

    El coste de un cache write es 1.25x el precio base — ligeramente más caro que una llamada normal. El coste de un cache read es 0.1x el precio base. Es decir, un 90% más barato.

    En un agent loop con 40 llamadas al día, pagas el 1.25x una vez. Las otras 39 veces pagas el 0.1x. La aritmética es brutal a tu favor.

    El TTL del caché

    El caché tiene un TTL (Time To Live) de 5 minutos por defecto. Mientras haya llamadas dentro de esa ventana, el caché se renueva automáticamente sin coste adicional. Si una conversación tiene mensajes frecuentes, el caché se mantiene activo.

    Existe también un TTL de 1 hora, que cuesta 2x el precio base en la escritura. Útil cuando tienes contextos que se reutilizan con menos frecuencia pero son muy costosos de regenerar.

    El mínimo de tokens para activar el caché

    No todo se puede cachear. El sistema exige un mínimo de tokens para crear una entrada de caché. Para claude-sonnet-4-6 y claude-opus-4-8, el mínimo es 1.024 tokens. Para claude-haiku-4-5, el umbral sube a 4.096 tokens — cuatro veces más alto, relevante si usas Haiku con prompts cortos. Si tu system prompt tiene menos tokens que el mínimo de tu modelo, el caché no se activa.

    En proyectos donde el system prompt es corto, la estrategia correcta es incluir el contexto del dominio directamente en el system prompt hasta superar ese umbral, o cachear las definiciones de herramientas junto con el sistema.


    Cómo habilitarlo: código TypeScript con el SDK oficial

    Aquí está el patrón que uso en producción. Nada de magia — tres cambios concretos en tu código.

    Habilitación básica: system prompt con cache_control

    import Anthropic from "@anthropic-ai/sdk";
    
    const client = new Anthropic();
    
    const response = await client.messages.create({
      model: "claude-sonnet-4-6",
      max_tokens: 1024,
      system: [
        {
          type: "text",
          text: `Eres un asistente especializado en análisis de documentos legales.
          
    Tu rol es:
    - Identificar cláusulas de riesgo en contratos
    - Resumir términos clave de forma clara y precisa
    - Señalar inconsistencias o ambigüedades legales
    - Comparar términos con estándares del sector
    
    [...aquí va el resto del system prompt extenso, con contexto del dominio,
    instrucciones detalladas, ejemplos de formato de respuesta, etc.
    Debe superar los 1.024 tokens para activar el caché...]`,
          cache_control: { type: "ephemeral" }, // <-- esto es todo lo que necesitas
        },
      ],
      messages: [
        {
          role: "user",
          content: "Analiza la cláusula de terminación de este contrato: ...",
        },
      ],
    });
    
    console.log(response.usage);
    

    En la primera llamada, usage mostrará:

    {
      "input_tokens": 45,
      "cache_creation_input_tokens": 1280,
      "cache_read_input_tokens": 0,
      "output_tokens": 312
    }
    

    En la segunda llamada (dentro de los 5 minutos):

    {
      "input_tokens": 45,
      "cache_creation_input_tokens": 0,
      "cache_read_input_tokens": 1280,
      "output_tokens": 289
    }
    

    cache_read_input_tokens tiene el 10% del coste. El system prompt completo se leyó desde caché. Esos 1.280 tokens no se procesaron desde cero.

    Cacheando herramientas y system prompt juntos

    Cuando tienes definiciones de herramientas largas — algo habitual en agentes con MCP o con múltiples funciones — el ahorro se multiplica. Aquí el patrón para cachear ambas cosas:

    import Anthropic from "@anthropic-ai/sdk";
    
    const client = new Anthropic();
    
    // Las definiciones de herramientas son estáticas — candidatas perfectas para caché
    const tools: Anthropic.Tool[] = [
      {
        name: "search_legal_database",
        description: `Busca en la base de datos legal precedentes y jurisprudencia relevante.
        Usa esta herramienta cuando necesites comparar cláusulas con casos anteriores o
        encontrar interpretaciones judiciales de términos específicos. La búsqueda incluye
        bases de datos de España, México, Argentina y Colombia. Devuelve hasta 10 resultados
        ordenados por relevancia con fecha, tribunal y resumen del caso.`,
        input_schema: {
          type: "object" as const,
          properties: {
            query: {
              type: "string",
              description: "Término o frase legal a buscar",
            },
            jurisdiction: {
              type: "string",
              enum: ["ES", "MX", "AR", "CO", "ALL"],
              description: "Jurisdicción a consultar",
            },
            date_range: {
              type: "string",
              description: "Rango de fechas en formato YYYY-YYYY",
            },
          },
          required: ["query"],
        },
      },
      {
        name: "analyze_clause_risk",
        description: `Analiza el nivel de riesgo de una cláusula contractual.
        Evalúa factores como onerosidad excesiva, cláusulas abusivas según legislación
        vigente, asimetría de obligaciones y exposición a penalidades. Devuelve un score
        de riesgo del 1 al 10 con justificación detallada y recomendaciones de negociación.`,
        input_schema: {
          type: "object" as const,
          properties: {
            clause_text: {
              type: "string",
              description: "Texto completo de la cláusula a analizar",
            },
            contract_type: {
              type: "string",
              description: "Tipo de contrato (laboral, mercantil, arrendamiento, etc.)",
            },
          },
          required: ["clause_text"],
        },
      },
      // cache_control al final del array de tools — marca el punto de caché
    ];
    
    // Añadimos cache_control al último tool para cachear todo el bloque
    const toolsWithCache = tools.map((tool, index) =>
      index === tools.length - 1
        ? { ...tool, cache_control: { type: "ephemeral" as const } }
        : tool
    );
    
    const response = await client.messages.create({
      model: "claude-sonnet-4-6",
      max_tokens: 2048,
      system: [
        {
          type: "text",
          text: "Eres un asistente especializado en análisis legal...",
          cache_control: { type: "ephemeral" }, // system prompt cacheado
        },
      ],
      tools: toolsWithCache, // tools cacheadas
      messages: [
        {
          role: "user",
          content: "¿Cuál es el riesgo de esta cláusula de no competencia?",
        },
      ],
    });
    

    Monitorizar el ahorro en tiempo real

    Esta función te dice exactamente cuánto has ahorrado en cada llamada:

    interface CostMonitor {
      inputTokensCost: number;
      cacheWriteCost: number;
      cacheReadCost: number;
      outputTokensCost: number;
      totalCost: number;
      savings: number;
      savingsPercent: number;
    }
    
    // Precios para claude-sonnet-4-6 por millón de tokens (en dólares)
    const PRICING = {
      input: 3.0,
      cacheWrite: 3.75, // 1.25x
      cacheRead: 0.3,   // 0.1x
      output: 15.0,
    };
    
    function calculateCallCost(usage: Anthropic.Usage): CostMonitor {
      const inputCost = (usage.input_tokens / 1_000_000) * PRICING.input;
      const cacheWriteCost =
        ((usage.cache_creation_input_tokens ?? 0) / 1_000_000) * PRICING.cacheWrite;
      const cacheReadCost =
        ((usage.cache_read_input_tokens ?? 0) / 1_000_000) * PRICING.cacheRead;
      const outputCost = (usage.output_tokens / 1_000_000) * PRICING.output;
    
      const totalCost = inputCost + cacheWriteCost + cacheReadCost + outputCost;
    
      // Coste hipotético sin caché (todos los tokens al precio base)
      const totalInputTokens =
        usage.input_tokens +
        (usage.cache_creation_input_tokens ?? 0) +
        (usage.cache_read_input_tokens ?? 0);
      const costWithoutCache =
        (totalInputTokens / 1_000_000) * PRICING.input + outputCost;
    
      const savings = costWithoutCache - totalCost;
      const savingsPercent =
        costWithoutCache > 0 ? (savings / costWithoutCache) * 100 : 0;
    
      return {
        inputTokensCost: inputCost,
        cacheWriteCost,
        cacheReadCost,
        outputTokensCost: outputCost,
        totalCost,
        savings,
        savingsPercent,
      };
    }
    
    // Uso:
    const monitor = calculateCallCost(response.usage);
    console.log(`Ahorro: $${monitor.savings.toFixed(6)} (${monitor.savingsPercent.toFixed(1)}%)`);
    

    Qué debes cachear y qué no

    Los mejores candidatos para el caché

    System prompts largos. Es el caso más obvio. Si tu system prompt tiene instrucciones de rol, reglas de formato, contexto del dominio y ejemplos, estás mirando fácilmente 2.000-8.000 tokens que se repiten en cada llamada. Cachear el system prompt es lo primero que debes activar.

    Definiciones de herramientas (tools). Especialmente en agentes con MCP o con muchas funciones. Las definiciones de tools incluyen nombres, descripciones detalladas y schemas completos. Pueden sumar 3.000-5.000 tokens fácilmente. Son siempre estáticas dentro de una sesión.

    Contexto de documentos. Si tu aplicación analiza un documento largo (un contrato, una especificación técnica, un PDF), ese documento va en el mensaje del usuario pero cambia muy poco. Puedes cachearlo con cache_control en el bloque del contenido del mensaje.

    Historial de conversación en agent loops. En un loop donde el agente tiene muchos turnos, cachear el historial acumulado evita pagar por reprocesar el contexto completo en cada iteración.

    Qué NO debes cachear

    El turno actual del usuario. Es el error más común. El mensaje que el usuario acaba de escribir cambia en cada llamada — si intentas cachearlo, el caché nunca tendrá un hit porque el contenido es siempre distinto.

    Tokens de extended thinking. Si usas extended thinking con Claude, los tokens del proceso de razonamiento interno no se cachean. Esto es relevante si estás midiendo ahorros en pipelines que usan thinking — los números no escalarán de la misma forma.

    Contenido que cambia con frecuencia. Si tienes un bloque de contexto que se actualiza cada pocos minutos (resultados de una búsqueda en tiempo real, estado de una sesión volátil), no tiene sentido marcarlo para caché porque nunca habrá un hit.

    Bloques demasiado pequeños. Si un bloque tiene menos de 1.024 tokens, el sistema no lo cacheará. No añadas cache_control a fragmentos pequeños — solo añade latencia sin beneficio.


    Comparación de coste: sin caching vs con caching

    Escenario real: un agente con 40 llamadas diarias durante 30 días.

    • System prompt: 5.000 tokens
    • Tools: 3.000 tokens
    • Pregunta del usuario: ~100 tokens (variable)
    • Respuesta del modelo: ~400 tokens (variable)
    • Modelo: claude-sonnet-4-6
    Escenario Coste por llamada Total mensual
    Sin caching (8.100 input + 400 output) $0.0303 $36.36
    Con caching — 1ª llamada del día (cache write 8.000 + 100 input + 400 output) $0.037
    Con caching — llamadas 2–40 (cache read 8.000 + 100 input + 400 output) $0.0084
    Con caching — total diario (1ª + 39 × $0.0084) $0.365/día $10.95

    Ahorro: 70%. Y esto asumiendo que el caché expira cada día. Con conversaciones más densas donde el TTL de 5 minutos se aprovecha bien, el ahorro sube al 85-90%.


    Preguntas frecuentes sobre prompt caching en Claude

    ¿El caché es compartido entre usuarios?
    No. El caché es privado por workspace de Anthropic. Desde febrero de 2026, hay aislamiento completo por workspace. Los datos de un usuario nunca se mezclan con los de otro.

    ¿Qué pasa si cambio el system prompt? ¿Se invalida el caché?
    Sí. El caché funciona por contenido exacto. Si modificas un solo carácter del bloque cacheado, se genera una nueva entrada de caché (cache write) en la siguiente llamada. El caché anterior expira según su TTL sin coste adicional.

    ¿Puedo cachear múltiples bloques en la misma llamada?
    Sí, hasta un máximo de cuatro breakpoints de caché por request. La restricción importante es el orden: los bloques con TTL más largo (1 hora) deben aparecer antes que los de TTL más corto (5 minutos) en la estructura del request.

    ¿El caching funciona con streaming?
    Sí. El prompt caching es compatible con la API de streaming de Claude. Los campos cache_creation_input_tokens y cache_read_input_tokens aparecen en el evento message_start del stream — no en message_delta. Es el primer evento emitido, antes de que lleguen los tokens de respuesta.


    El siguiente nivel: combinar con Claude Code

    Si ya estás explorando agentes más complejos, el prompt caching cambia la ecuación de coste de forma radical. Un agent loop sin caching que hace 10 iteraciones paga los tokens del system prompt y las tools diez veces. Con caching, los paga una vez y lee el resto.

    En Claude Code: Effort, Models, Tools y Context hay una sección completa sobre cómo gestiona Claude Code el contexto en agent loops largos — es el contexto perfecto para entender dónde encaja el caching a nivel de infraestructura.

    Y si quieres construir productos reales sobre la API de Anthropic con esta clase de optimizaciones ya integradas desde el primer sprint, el curso Construye con IA: De la Idea al Producto con Claude cubre el stack completo — desde la arquitectura del agente hasta el control de costes en producción.


    Lo que puedes hacer hoy

    Si tienes una aplicación que usa la API de Claude en producción, abre el código y busca dónde defines el system prompt. Si es una cadena de texto plana, conviértela en un array con cache_control: { type: "ephemeral" }.

    Eso solo. Una línea de cambio. Comprueba la siguiente factura.

    Si además tienes tools largas, aplica el mismo patrón al último elemento del array de herramientas. Tendrás dos puntos de caché activos y el ahorro será inmediato.

    El prompt caching no es una optimización avanzada que requiere rediseñar tu arquitectura. Es una configuración de tres minutos que debería estar activa en cualquier aplicación seria sobre la API de Claude. Si no la tienes, estás pagando de más desde el primer día.


    Bezael Pérez — Fundador de Dominicode. Developer senior con 15+ años construyendo software. Si construyes con IA y quieres profundizar más allá de los tutoriales, en Dominicode Labs estamos trabajando en proyectos reales con la API de Anthropic, arquitecturas de agentes y todo lo que no cabe en un post.

  • Claude Fable 5 vuelve: qué pasó y qué cambia para developers

    Claude Fable 5 vuelve: qué pasó y qué cambia para developers

    El 12 de junio de 2026, Anthropic apagó Claude Fable 5 de golpe.

    Sin aviso previo. Sin fecha de vuelta. Sin explicación técnica completa. El modelo que llevaba apenas tres días disponible desapareció para todos los usuarios del planeta — Europa, Latinoamérica, Asia, todos — porque el gobierno de EE.UU. no podía verificar nacionalidades en tiempo real y decidió cortar el acceso global en lugar de arriesgarse.

    Ese mismo día, developers de medio mundo abrieron Claude.ai y encontraron un modelo degradado. Los que habían empezado a construir pipelines con Fable 5 tuvieron que pivotar sobre la marcha. Y los que llevábamos años viendo cómo la IA maduraba como industria recibimos un recordatorio brutal: cuando un modelo tiene capacidades que un Estado considera amenaza para la seguridad nacional, el interruptor lo tiene el Estado, no Anthropic.

    Hoy, 1 de julio de 2026, Claude Fable 5 vuelve. Y la historia de cómo llegamos hasta aquí dice más sobre el futuro de la IA que cualquier benchmark.


    Lo que pasó: el jailbreak que lo cambió todo

    Investigadores de Amazon descubrieron una técnica que permitía a Fable 5 identificar vulnerabilidades en software y, en al menos un caso documentado, demostrar cómo explotarlas. El gobierno de EE.UU. reaccionó con rapidez: el mismo día 12 de junio, el Departamento de Comercio aplicó controles de exportación de emergencia que afectaron tanto a Fable 5 como a Mythos 5.

    La tensión entre ambas partes fue pública. El gobierno argumentó que el problema podría haberse corregido antes de la suspensión. Anthropic respondió que la técnica era más estrecha y específica de lo que la orden de emergencia implicaba — no una vulnerabilidad sistémica, sino un vector concreto que requerirían semanas de investigación para reproducir.

    El debate sobre la severidad real del jailbreak sigue abierto. El resultado fue inequívoco: controles de exportación de emergencia, suspensión global, y Anthropic sin poder verificar la nacionalidad de sus usuarios en tiempo real.

    No había otra salida. Apagaron todo.


    Fable 5 y Mythos 5: la diferencia que importa

    Aquí hay un matiz que mucha cobertura mediática perdió.

    Mythos 5 es la denominación interna de los modelos con capacidades cibernéticas más avanzadas que Anthropic ha construido jamás — superiores a cualquier otro modelo del mercado en ese dominio. Tras la suspensión, Anthropic decidió que Mythos 5 solo estará disponible para socios del Proyecto Glasswing, un programa de ciberseguridad defensiva con acceso controlado y supervisión directa.

    Fable 5 es diferente. Es el modelo de propósito general que se lanza hoy con los salvaguardas más fuertes que Anthropic ha implementado en ningún modelo de su historia. Anthropic afirma explícitamente que Fable 5 "no proporciona capacidades ofensivas únicas" — es decir, no hace nada que un atacante sofisticado no pudiera hacer con las herramientas que ya existen.

    Fable 5 Mythos 5
    Propósito General (razonamiento, código, escritura) Ciberseguridad avanzada
    Acceso Público (planes de pago) Solo Proyecto Glasswing
    API pública ✅ Sí ❌ No
    Capacidades ofensivas No únicas respecto a herramientas existentes Superiores a cualquier otro modelo del mercado

    La distinción es importante para cualquier developer que esté construyendo con la API. No estás usando Mythos 5. Estás usando Fable 5, que ha pasado por una revisión de seguridad que ningún modelo anterior había tenido.


    Qué cambió en Claude Fable 5: los nuevos salvaguardas

    Anthropic no volvió con el mismo modelo. Volvió con un clasificador de seguridad reentrenado específicamente para detectar y bloquear la técnica descrita en el reporte de Amazon.

    Según el anuncio oficial de Anthropic, el nuevo clasificador bloquea el comportamiento problemático en más del 99% de los casos. Cuando se activa, la solicitud no falla en silencio — se redirige automáticamente a Claude Opus 4.8. El usuario recibe respuesta, pero sin las capacidades que generaron el problema.

    El mecanismo de defensa tiene tres capas:

    1. El entrenamiento base del modelo, que ya rechaza asistencia con solicitudes peligrosas.
    2. Un clasificador específico para el patrón de jailbreak identificado por Amazon.
    3. Un margen de seguridad ampliado — Anthropic subió el umbral de bloqueo de forma deliberada, asumiendo más falsos positivos para reducir el riesgo de usos maliciosos.

    Ese tercer punto es el que más impacta a developers en producción. Más falsos positivos significa que algunas solicitudes legítimas relacionadas con ciberseguridad, análisis de código o auditoría de vulnerabilidades van a llegar a Opus 4.8 en lugar de Fable 5. No es un bug. Es una decisión consciente de arquitectura de seguridad.

    El Departamento de Comercio de EE.UU. verificó los salvaguardas y los calificó de "extraordinariamente fuertes". El 30 de junio levantó los controles de exportación. El 1 de julio, Fable 5 vuelve.


    Disponibilidad desde hoy: lo que necesitas saber

    La reactivación es global desde el 1 de julio en Claude.ai, Claude Platform, Claude Code y Claude Cowork.

    AWS, Google Cloud y Microsoft Foundry se reactivarán "lo antes posible" — sin fecha concreta confirmada.

    Hay un período de transición con límites temporales:

    • Hasta el 7 de julio: planes Pro, Max, Team y empresas seleccionadas tienen acceso a Fable 5 con hasta el 50% de sus límites de uso semanal habituales.
    • Después del 7 de julio: disponible mediante créditos de uso, sin restricción porcentual.

    Si estás en el plan gratuito, no hay cambios respecto a antes de la suspensión. Fable 5 era y sigue siendo acceso de pago.

    Para los que construimos con la API de Anthropic, el modelo vuelve a estar disponible desde hoy. Si tenías pipelines configurados con Fable 5 antes del 12 de junio, probablemente ya están activos de nuevo. Verifica tu dashboard y el comportamiento del clasificador con tus casos de uso específicos — especialmente si tienes prompts relacionados con análisis de código o seguridad.


    El nuevo marco de evaluación de jailbreaks

    Lo más interesante de lo que Anthropic publicó esta semana no son los salvaguardas. Es el marco que proponen como estándar industrial para evaluar la severidad de un jailbreak.

    Cuatro criterios:

    1. Ganancia de capacidad. ¿Cuánto supera lo que ya existe? Un jailbreak que replica lo que hace una herramienta de código abierto pesa menos que uno que desbloquea algo genuinamente nuevo.

    2. Amplitud. ¿Cuántas tareas ofensivas distintas habilita? Un jailbreak muy específico (un tipo de ataque, un vector) no es lo mismo que uno que abre la puerta a toda una clase de capacidades.

    3. Facilidad de armamento. ¿Cuánto esfuerzo humano experto requiere convertir el output en un ataque real? Hay una diferencia enorme entre "el modelo identifica una vulnerabilidad" y "el modelo produce un exploit listo para ejecutar".

    4. Descubribilidad. ¿Cómo de fácil es que un actor malicioso llegue a esta técnica? Un jailbreak que requiere semanas de ingeniería de prompts por parte de investigadores avanzados no tiene el mismo riesgo que uno que circula en un foro público.

    Este marco no es solo teoría. Anthropic lo propone como base para que gobiernos, empresas y laboratorios de IA puedan hablar de jailbreaks con criterios objetivos en lugar de reacciones políticas de emergencia.

    Si trabajas en seguridad o builds productos con IA, este marco te va a ser útil.


    Lo que esto significa para developers que construyen con IA

    Hace tres semanas, el modelo más capaz del mercado desapareció sin fecha de vuelta. Hoy está de vuelta con salvaguardas que ningún modelo anterior había tenido, respaldado por verificación gubernamental y un nuevo marco de evaluación que puede convertirse en estándar.

    ¿Qué cambia para nosotros?

    Primero, la confirmación de algo que debíamos asumir pero que muchos ignoraban: los modelos más capaces van a estar regulados. No es una posibilidad futura. Es el presente. El mismo día que Fable 5 volvió, Mythos 5 quedó restringido a socios controlados del Proyecto Glasswing. La IA de alto impacto va a tener fricción institucional. Cuanto antes lo integremos en nuestra planificación de producto, mejor.

    Segundo, la arquitectura de fallback importa más de lo que pensamos. Si tu producto dependía de Fable 5 el 12 de junio, tuviste un problema durante diecinueve días. Los mejores sistemas tienen fallback a modelos alternativos — no porque anticipen este escenario exacto, sino porque construyen con redundancia desde el principio.

    Tercero, y esto es lo más importante: la madurez del sector se mide en cómo responde a los errores, no en si los comete. Anthropic tardó diecinueve días en volver. En esas tres semanas entrenaron un nuevo clasificador, pasaron una auditoría gubernamental, propusieron un marco de evaluación de jailbreaks que puede convertirse en estándar, y redefinieron el acceso a Mythos 5. Eso no es una crisis mal gestionada. Es una empresa que aprendió en tiempo real bajo presión máxima.

    Nosotros podemos hacer lo mismo en nuestros productos.

    En el curso Construye con IA hablo de esto en profundidad: cómo construir sistemas que no colapsen cuando el modelo subyacente cambia, se actualiza o desaparece temporalmente. La resiliencia arquitectural no es un añadido. Es la condición de base para cualquier producto serio con IA.


    Preguntas frecuentes sobre Claude Fable 5

    ¿Claude Fable 5 está disponible hoy para todos los usuarios?

    Desde el 1 de julio de 2026, Fable 5 está disponible en Claude.ai, Claude Platform, Claude Code y Claude Cowork para usuarios en todos los países. AWS, Google Cloud y Microsoft Foundry se reactivarán próximamente. El acceso a Fable 5 requiere un plan de pago (Pro, Max, Team o Enterprise).

    ¿Qué es el jailbreak que causó la suspensión de Claude Fable 5?

    Investigadores de Amazon descubrieron una técnica de prompting que permitía a Fable 5 identificar vulnerabilidades en software y, en al menos un caso, demostrar cómo explotarlas. El gobierno de EE.UU. aplicó controles de exportación de emergencia el 12 de junio de 2026, lo que llevó a Anthropic a suspender el acceso global porque no podía verificar la nacionalidad de sus usuarios en tiempo real.

    ¿Cuál es la diferencia entre Claude Fable 5 y Mythos 5?

    Fable 5 es el modelo de propósito general disponible desde hoy para el público. Mythos 5 es la denominación de los modelos con capacidades cibernéticas avanzadas — superiores a cualquier otro modelo del mercado — restringido exclusivamente a socios del Proyecto Glasswing para ciberseguridad defensiva. No es accesible a través de la API pública.

    ¿Cómo afectan los nuevos salvaguardas al uso de Fable 5 en desarrollo de software?

    El nuevo clasificador bloquea el patrón de jailbreak en más del 99% de los casos, redirigiendo esas solicitudes a Claude Opus 4.8. Anthropic aumentó deliberadamente el margen de seguridad, lo que genera más falsos positivos en tareas de análisis de código, auditoría de seguridad o detección de vulnerabilidades. Si tu caso de uso incluye estas áreas, testea tu pipeline con Fable 5 para verificar el comportamiento del clasificador.

    ¿Qué límites de uso tiene Claude Fable 5 tras la vuelta?

    Hasta el 7 de julio de 2026, los planes Pro, Max, Team y empresas Enterprise seleccionadas tienen acceso a Fable 5 con hasta el 50% de sus límites de uso semanal habituales. Después del 7 de julio, el modelo estará disponible mediante créditos de uso sin restricción porcentual.

    ¿Puede volver a ocurrir una suspensión similar con otros modelos de Anthropic?

    Sí. Los controles de exportación son un instrumento legal que el gobierno de EE.UU. puede aplicar a cualquier modelo con capacidades que considere una amenaza. La colaboración reforzada entre Anthropic y el gobierno reduce la probabilidad de una suspensión de emergencia, pero no la elimina. Cualquier arquitectura de producto con IA debe contemplar escenarios de indisponibilidad del modelo principal.


    Si quieres estar al día de cómo estos eventos impactan a los developers que construyen con IA, en Dominicode Labs analizamos en tiempo real las decisiones de los grandes laboratorios y sus implicaciones para producción. Y en el canal de YouTube seguiré cubriendo la evolución de Fable 5 en las próximas semanas.


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

  • Claude Sonnet 5: el modelo que trabaja solo mientras tú duermes

    Claude Sonnet 5: el modelo que trabaja solo mientras tú duermes

    Me pasó hace unos meses revisando el output de un agente que había dejado corriendo toda la noche.

    Esperaba encontrar la tarea a medias. Un formulario sin completar. Alguna herramienta mal llamada. Lo habitual con los modelos de la generación anterior: empezaban bien, pero a mitad de camino se perdían, pedían confirmación o simplemente paraban.

    En cambio, encontré el trabajo terminado. Del principio al fin. Sin intervención.

    Ese momento cambia algo en tu cabeza como developer. No es que la IA sea "mejor". Es que ya no necesita que estés mirando.

    Claude Sonnet 5 es exactamente esa promesa hecha modelo. Anthropic lo lanzó el 30 de junio de 2026 y lo describe como "el modelo Sonnet más agéntico hasta la fecha". No es marketing vacío — la diferencia en tareas autónomas y multi-paso es medible y, para quien construye con IA, es relevante desde el primer día.


    Por qué Sonnet 5 es distinto a todo lo anterior

    Hasta ahora, la frontera estaba clara: si querías un agente que realmente terminase el trabajo, necesitabas Opus. Sonnet era el punto medio — rápido, accesible, suficientemente bueno para tareas simples. Pero en flujos complejos con múltiples pasos, herramientas y decisiones encadenadas, Sonnet se quedaba corto.

    Claude Sonnet 5 rompe esa frontera.

    Anthropic no ha simplemente subido los parámetros. Han optimizado específicamente para comportamiento agéntico: planificación de tareas, uso coordinado de herramientas (navegadores, terminales, APIs), y lo más relevante — la capacidad de verificar su propio resultado sin que se lo pidas.

    Eso último importa más de lo que parece. Un modelo que ejecuta código y luego comprueba si el output es el esperado, sin que tú se lo digas, está un paso más cerca de un colaborador que de una herramienta.


    Las capacidades agénticas en detalle

    Hay tres áreas donde el cambio es palpable:

    Tareas multi-paso sin interrupciones. Modelos anteriores tendían a pedir confirmación o detenerse cuando encontraban ambigüedad. Sonnet 5 mantiene el hilo. Algunos partners de Anthropic reportan que "terminó el trabajo de principio a fin sin intervención" — algo que antes era territorio exclusivo de Opus 4.

    Uso de herramientas coordinado. Puede combinar búsqueda web, ejecución de código y llamadas a APIs en la misma tarea sin perder el contexto de lo que estaba haciendo. No es nuevo que los modelos puedan usar herramientas — lo nuevo es que lo hacen con coherencia a lo largo de cadenas largas de razonamiento.

    Auto-verificación del resultado. Si ejecuta una query de base de datos o genera un archivo, puede evaluar si el resultado tiene sentido antes de dártelo. Esto reduce drásticamente la necesidad de loops de revisión en tus agentes.

    Si estás construyendo con la API de Claude o con Claude Code, estas tres capacidades cambian el diseño de tus flujos. No necesitas los mismos guardrails de antes. No necesitas los mismos puntos de control manual.

    En el curso de Construye con IA cubrimos exactamente este tipo de arquitectura de agentes — y con Sonnet 5 muchos de esos patrones se simplifican considerablemente.


    Benchmarks: qué dicen los números

    Los benchmarks importan, pero necesitan contexto. Aquí va la comparativa relevante para developers:

    Benchmark Claude Sonnet 5 Claude Sonnet 4.6 Claude Opus 4.8
    BrowseComp Superior Base de comparación Superior
    OSWorld-Verified Superior Base de comparación Superior
    Razonamiento general Muy cercano a Opus Inferior Referencia
    Codificación Notable mejora Base Referencia
    Esfuerzo "extra high" Iguala a Opus 4.8 Referencia

    Evaluaciones cualitativas basadas en el anuncio oficial de Anthropic (30 jun 2026).

    El dato más interesante: a nivel de esfuerzo máximo, Sonnet 5 iguala a Opus 4.8. Esto es arquitectónicamente significativo. Significa que para la mayoría de las tareas que antes justificaban pagar el precio de Opus, ahora puedes usar Sonnet 5 a un coste mucho menor.

    La excepción es ciberseguridad. Anthropic es explícito: Sonnet 5 no fue entrenado deliberadamente para tareas de seguridad ofensiva, y Opus 4.8 sigue siendo superior en ese dominio específico.


    Precios y disponibilidad

    Plan Precio hasta 31 ago 2026 Precio desde 1 sep 2026
    Input tokens $2 / M tokens $3 / M tokens
    Output tokens $10 / M tokens $15 / M tokens

    Anthropic ha aplicado un precio introductorio hasta finales de agosto. Si estás evaluando el switch en la API, este es el momento óptimo para hacerlo.

    Dónde está disponible:

    • Modelo predeterminado en los planes Free y Pro de Claude.ai
    • Disponible en Max, Team, Enterprise
    • Claude Code
    • API (model ID: claude-sonnet-5)

    Si usas Claude.ai directamente, ya lo tienes — es el modelo por defecto desde el lanzamiento.


    El tokenizador actualizado: impacto práctico

    Este punto se menciona poco y puede sorprenderte en producción.

    Sonnet 5 usa un tokenizador actualizado similar al que se introdujo con Opus 4.7. El resultado es que el mismo texto que antes ocupaba X tokens ahora puede ocupar entre 1.0× y 1.35× más tokens.

    ¿Qué significa esto en la práctica?

    Si tienes prompts largos con contexto extenso (documentos, conversaciones, sistemas de RAG), tu consumo de tokens aumentará. Anthropic compensa esto con el precio introductorio, pero necesitas tener este factor en cuenta al proyectar costes para producción.

    Una regla rápida: si venías de Sonnet 4.6 y tienes prompts de más de 5.000 tokens, haz una prueba controlada antes de cambiar el modelo en producción. Mide el consumo real, no lo estimes desde los benchmarks públicos.

    Para esto el post sobre prompt caching en Claude es directamente aplicable — con el nuevo tokenizador, el caching se vuelve aún más relevante para controlar costes.


    Cómo empezar hoy

    En Claude.ai: Ya está activo. Es el modelo por defecto. No necesitas hacer nada.

    En la API:

    El siguiente ejemplo muestra la integración mínima con el SDK oficial @anthropic-ai/sdk para TypeScript. El único cambio respecto a modelos anteriores es el model ID: claude-sonnet-5.

    import Anthropic from "@anthropic-ai/sdk";
    
    const client = new Anthropic();
    
    const response = await client.messages.create({
      model: "claude-sonnet-5",
      max_tokens: 4096,
      messages: [
        {
          role: "user",
          content: "Analiza este código y sugiere mejoras de rendimiento...",
        },
      ],
    });
    

    El cambio de model ID es inmediato. Si tienes un sistema en producción con claude-sonnet-4-6, cambiar a claude-sonnet-5 no requiere modificar nada más en la integración básica.

    Si usas Claude Code, el modelo ya está disponible y puedes seleccionarlo desde la configuración del cliente.


    Qué significa esto para developers que construyen con IA

    Voy a ser directo porque creo que es lo que necesitas saber.

    El lanzamiento de Sonnet 5 no es un update de rendimiento incremental. Es un reposicionamiento del tier medio.

    Hasta ahora, la decisión era: velocidad y coste (Haiku/Sonnet) vs. capacidad y razonamiento complejo (Opus). Con Sonnet 5, esa brecha se cierra de forma significativa. Puedes construir agentes que realmente terminen el trabajo, a un coste que tiene sentido para producción.

    Para quien está construyendo productos con IA — y no solo experimentando — esto cambia la ecuación de build vs. cost. Puedes subir la ambición de tus agentes sin subir proporcionalmente el presupuesto.

    El riesgo que veo es el de siempre: sobreestimar la autonomía del modelo en los primeros días. Sonnet 5 es notablemente mejor en tareas autónomas, pero sigue siendo un modelo de lenguaje. Sigue necesitando specs claras, herramientas bien definidas y tests que verifiquen los outputs.

    En Dominicode Labs estamos ya trabajando con Sonnet 5 en los proyectos de la comunidad — si quieres ver cómo se integra en flujos reales de producción, es donde está pasando.


    Preguntas frecuentes sobre Claude Sonnet 5

    ¿Qué diferencia hay entre Claude Sonnet 5 y Claude Sonnet 4.6?

    Claude Sonnet 5 está optimizado para comportamiento agéntico: puede planificar y ejecutar tareas multi-paso sin interrupciones, verificar sus propios resultados automáticamente y usar herramientas (navegadores, terminales, APIs) de forma coordinada en cadenas largas de razonamiento. Sonnet 4.6 era capaz, pero tendía a detenerse o pedir confirmación en tareas complejas. La mejora en benchmarks como BrowseComp y OSWorld-Verified refleja exactamente esta diferencia en tareas autónomas.

    ¿Es Claude Sonnet 5 tan bueno como Claude Opus 4.8?

    En la mayoría de tareas cotidianas de codificación, razonamiento y trabajo de conocimiento, Sonnet 5 está muy cerca de Opus 4.8 — y a nivel de esfuerzo máximo puede igualarlo. La excepción es el dominio de ciberseguridad, donde Opus 4.8 sigue siendo superior porque fue entrenado deliberadamente para esas tareas. Para el 90% de los casos de uso de desarrollo con IA, Sonnet 5 es suficientemente capaz y mucho más accesible en precio.

    ¿Cómo afecta el nuevo tokenizador a mis costes en la API?

    El tokenizador actualizado puede incrementar el consumo de tokens en un factor de 1.0× a 1.35× respecto a modelos anteriores. Esto es especialmente relevante con contextos largos: documentos, conversaciones extendidas, sistemas RAG. Anthropic compensa esto con el precio introductorio vigente hasta el 31 de agosto de 2026. La recomendación práctica es medir el consumo real con tus prompts de producción antes de estimar costes a escala.

    ¿Puedo usar Claude Sonnet 5 en Claude Code?

    Sí. Claude Sonnet 5 está disponible en Claude Code desde el lanzamiento. Dado que Claude Code es una herramienta diseñada precisamente para tareas agénticas de desarrollo — escribir, ejecutar, verificar código de forma autónoma — la combinación con Sonnet 5 es especialmente potente. Puedes seleccionar el modelo desde la configuración del cliente.

    ¿Qué precio tiene Claude Sonnet 5 y cuándo cambia?

    El precio introductorio vigente hasta el 31 de agosto de 2026 es $2/M tokens de input y $10/M tokens de output. A partir del 1 de septiembre de 2026 pasa a $3/M input y $15/M output. Si estás evaluando la migración en producción, hacerlo antes de septiembre tiene sentido económico.

    ¿Claude Sonnet 5 ya está disponible en el plan gratuito de Claude.ai?

    Sí. Desde el lanzamiento el 30 de junio de 2026, Claude Sonnet 5 es el modelo predeterminado en todos los planes de Claude.ai, incluyendo el gratuito. No necesitas hacer ningún cambio — si abres Claude.ai hoy, ya estás usando Sonnet 5.


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