Si estás construyendo una aplicación o un agente autónomo de IA, tu archivo de configuración de claves de API probablemente se parezca a una pesadilla. Tienes un token de facturación para OpenAI, otro para Anthropic, otro para los modelos de Google, y quizás una cuenta en un hosting de GPUs externas para los modelos de código abierto.
Facturas fragmentadas, SDKs de código distintos y un dolor de cabeza constante cada vez que un proveedor sube los precios o sufre una caída de servicio.
Hay una forma mucho más inteligente de gestionar esto.
Hoy te quiero explicar qué es openrouter y para qué sirve, y por qué se ha convertido en la herramienta de backend indispensable para cualquier desarrollador de inteligencia artificial moderna.
¿Qué es OpenRouter?
OpenRouter (disponible en openrouter.ai) es un enrutador y pasarela de API unificada para modelos de lenguaje. Actúa como un intermediario o agregador: tú te conectas a OpenRouter con una única clave de API y, a cambio, ellos te dan acceso a cientos de modelos distintos de múltiples proveedores de forma instantánea.
En lugar de registrarte en Anthropic, OpenAI, Google Cloud, Meta y DeepSeek por separado, solo te registras en OpenRouter, cargas saldo en una sola cuenta y consumes los modelos que necesites pagando estrictamente por el uso de tokens.
¿Para qué sirve OpenRouter en el día a día?
Si eres desarrollador de software o integras IA en tus flujos de negocio, OpenRouter resuelve cuatro problemas de infraestructura masivos:
1. Unificación de SDKs (API compatible con OpenAI)
No necesitas aprender a usar la librería de Anthropic o los formatos específicos de Google. OpenRouter utiliza el formato estándar de la API de OpenAI. Para cambiar de modelo, solo tienes que cambiar una string de texto en tu llamada, sin tocar una sola línea de código. Como vimos en nuestro análisis de Qwen 3.7 y OpenRouter, esto te permite cambiar de modelo en caliente para aprovechar el contexto extendido y las capacidades de razonamiento.
2. Acceso a Modelos Propietarios y Open-Source
En la misma plataforma conviven los gigantes de pago (como GPT-4o o Gemini 1.5 Pro) junto con las versiones de código abierto servidas a alta velocidad (como Qwen 2.5 Coder o DeepSeek-R1). Esto te permite experimentar con docenas de variantes en segundos.
3. Redundancia y Fallbacks
Si la API oficial de Anthropic se cae o sufre congestión, puedes configurar tu código para que OpenRouter derive la petición automáticamente a un modelo equivalente (como GPT-4o) de forma transparente para el usuario final, garantizando que tu aplicación nunca deje de responder.
4. Control de Costes y Estadísticas
La plataforma ofrece un panel visual increíblemente detallado que te muestra qué modelos están consumiendo más recursos, cuántos tokens envías en la fase de contexto y la latencia promedio de cada proveedor.
Cómo implementarlo en tus Agentes Autónomos
Integrar OpenRouter en frameworks agénticos como Hermes es sumamente sencillo. Al ser compatible con la especificación de OpenAI, basta con apuntar el base_url del cliente al endpoint unificado:
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://openrouter.ai/api/v1",
apiKey: "tu_token_de_openrouter_aqui",
});
async function main() {
const completion = await client.chat.completions.create({
model: "anthropic/claude-3.5-sonnet", // Cambia a "qwen/qwen-2.5-coder-32b" cuando quieras
messages: [
{ role: "user", content: "Escribe una función de ordenamiento en TypeScript." }
],
});
console.log(completion.choices[0].message.content);
}
main();
Este enfoque híbrido y unificado es la infraestructura de base que montamos en el curso de Construye con IA para desarrollar productos escalables, y el que explotamos en producción para conectar canales de comunicación automatizados en el nuevo curso de Hermes Agent.
Conclusión: La API definitiva para Developers
No pierdas el tiempo gestionando contratos de facturación individuales ni peleando con diferentes SDKs. Al adoptar OpenRouter en tus desarrollos de inteligencia artificial, simplificas tu código a una única conexión, blindas tu aplicación contra caídas de proveedores y tienes la libertad de elegir el modelo idóneo para cada tarea con un solo cambio de configuración.
Si estás utilizando OpenRouter en producción y quieres optimizar el consumo de tus tokens en tareas agénticas complejas, te espero en Dominicode Labs.
Preguntas Frecuentes (FAQ)
¿OpenRouter cobra alguna comisión por encima del precio del modelo?
No. OpenRouter ofrece los modelos a los precios de coste oficiales de los proveedores (e incluso más baratos en algunos modelos de código abierto debido a acuerdos de volumen con servidores de hosting de inferencia como Together o Fireworks). Su modelo de negocio se basa en la agregación y volumen de tráfico.
¿Qué sucede con la privacidad de mis datos al usar OpenRouter?
OpenRouter actúa como un proxy de transporte. Tus prompts viajan encriptados a través de su API hacia el proveedor del modelo seleccionado. La plataforma no almacena las conversaciones de tus usuarios a menos que actives explícitamente los logs en tu panel de configuración para depuración de errores.
¿Se pueden usar herramientas (Tool Calling) a través de OpenRouter?
Sí. OpenRouter soporta el paso de herramientas (tools) y la llamada de funciones nativas para todos los modelos que lo admitan de forma nativa en su arquitectura (como las familias de Claude, GPT, Llama y Qwen).
¿Es seguro usar OpenRouter para aplicaciones de producción masivas?
Sí, es una infraestructura robusta utilizada por miles de aplicaciones y startups en producción a nivel global. Al contar con endpoints optimizados y soporte para conmutación por error (fallbacks), suele ofrecer una estabilidad incluso superior a la de conectarse directamente a un único proveedor.
Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.
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.
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.
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.
Un junior del equipo me enseñó hace poco un computed() que calculaba el total de un carrito. Funcionaba. Pero me dijo una frase que lo delata todo: “le metí un console.log dentro y no se imprime cuando cambio la cantidad… hasta que abro el modal del total”.
No estaba roto. Estaba haciendo exactamente lo que debe hacer.
El problema no era su código. Era su modelo mental. No conocía el grafo reactivo de Angular, la estructura que decide qué se recalcula y cuándo. Pensaba que un computed() se recalcula cuando cambian sus datos. Y no. Se recalcula cuando alguien lo lee. Esa diferencia, que parece un detalle, es la puerta de entrada a entender cómo piensa Angular por dentro.
Porque eso es justo lo que vive debajo de signal(), computed() y effect(): un grafo que casi nadie se molesta en entender, y que lo explica todo.
¿Qué es el grafo reactivo de Angular?
El grafo reactivo de Angular es la estructura interna que el framework construye con su sistema de Signals para saber, en todo momento, qué valores dependen de qué otros. No es una API que tú llamas. Es el motor que se monta solo cuando declaras signals, computeds y effects, y es lo que permite que Angular recalcule únicamente lo que cambió en lugar de revisar la aplicación entera.
Los Signals son estables desde Angular 16-17 (2023), y son la base sobre la que se apoya el modo zoneless, disponible como opción de producción a partir de Angular v20.
Imagínalo literalmente como un grafo: nodos conectados por flechas. Los nodos son tus valores reactivos. Las flechas son las dependencias entre ellos. Cuando un valor cambia, Angular recorre esas flechas para decidir qué tocar y qué dejar en paz.
Y la clave —la que casi nadie explica— es que esas flechas no las dibujas tú. Las descubre Angular en tiempo de ejecución.
Vamos por partes.
Los nodos: productores y consumidores
Todo en el grafo es una de dos cosas (o las dos a la vez). Te lo presento como modelo conceptual, no como API pública: Angular no te expone estos nombres, pero entenderlos cambia cómo lees tu propio código.
Un signal() es un productor puro. Tiene un valor, otros lo leen, pero él no depende de nadie. Es una raíz del grafo.
Un computed() es consumidor y productor a la vez. Lee otros signals (consume) y a su vez otros lo leen a él (produce). Es un nodo intermedio.
Un effect() es un consumidor puro. Lee signals y reacciona, pero nadie lee a un effect. Es una hoja del grafo, el final de la cadena.
El grafo aquí tiene una forma clarísima: precio y cantidad apuntan a total, y total apunta al effect. Cuatro nodos, tres flechas.
Pero tú no escribiste ni una sola de esas flechas.
Las aristas: tracking dinámico de dependencias
Aquí está la primera idea que separa a quien usa signals de quien los entiende.
Las dependencias no se declaran. Angular las descubre.
Cuando un computed() o un effect() se ejecuta, Angular activa un registro temporal: “todo signal que se lea durante esta ejecución se anota como dependencia”. Lees precio() dentro del computed → se crea la flecha precio → total. Lees cantidad() → se crea cantidad → total. Termina la ejecución, se cierra el registro.
Esto tiene una consecuencia preciosa: las dependencias pueden ser condicionales. Cada ejecución puede producir un conjunto distinto de aristas.
const modoOscuro = signal(false);
const colorClaro = signal('#ffffff');
const colorOscuro = signal('#1a1a1a');
const colorFondo = computed(() => {
if (modoOscuro()) {
return colorOscuro(); // solo se lee si modoOscuro es true
}
return colorClaro(); // solo se lee si modoOscuro es false
});
Cuando modoOscuro es false, este computed depende de modoOscuro y de colorClaro. No depende de colorOscuro en absoluto. Si cambias colorOscuro mientras estás en modo claro, colorFondo no se marca como sucio, no se recalcula, no pasa nada.
Cambia modoOscuro a true y, en el siguiente recálculo, el grafo se reconfigura: ahora la flecha sale de colorOscuro y la de colorClaro desaparece.
Esto no lo consigues gratis con RxJS combinando observables. Aquí es el comportamiento por defecto, sin esfuerzo. Es exactamente este tipo de detalle el que trabajamos a fondo en el curso de Angular Moderno, porque entender el grafo cambia cómo estructuras el estado de toda la app.
Push y pull: por qué el computed de mi compañero no se ejecutaba
Volvamos a la historia del principio. El console.log que no se imprimía.
El grafo reactivo funciona con dos fases distintas, y casi todo el mundo solo conoce la primera.
Fase push (cuando cambias un signal). Llamas a cantidad.set(5). Angular recorre el grafo hacia abajo y marca a los consumidores como “sucios” (stale). total se marca sucio. El effect que depende de total se marca sucio. Y ya. No se recalcula nada todavía. Solo se propaga una marca de “esto podría haber cambiado”.
Fase pull (cuando alguien lee). El valor de un computed() solo se recalcula cuando alguien lo lee y está marcado sucio. Es perezoso (lazy) y memoizado: si nadie lo lee, no se ejecuta jamás.
const a = signal(1);
const b = signal(2);
const suma = computed(() => {
console.log('¡Calculando suma!'); // ¿cuándo se imprime esto?
return a() + b();
});
a.set(10);
a.set(20);
a.set(30);
// Hasta aquí: el log NO se ha impreso ni una vez.
console.log(suma()); // AHORA imprime "¡Calculando suma!" y luego 32
console.log(suma()); // NO vuelve a imprimir: valor memoizado
Tres cambios en a y cero recálculos, porque nadie leyó suma. La leemos una vez y se calcula una vez. La leemos de nuevo sin cambios de por medio y devuelve el valor cacheado sin recalcular.
Por eso el computed() del carrito “no se ejecutaba” hasta abrir el modal: ningún template estaba leyendo ese valor. En cuanto el modal lo renderizó, lo leyó, y entonces —y solo entonces— se recalculó.
No era un bug. Era el grafo trabajando exactamente como debe: no malgastar ni un ciclo de CPU en valores que nadie está mirando.
Consistencia glitch-free: nunca verás un estado intermedio falso
Pregunta incómoda: ¿qué pasa cuando un nodo depende del mismo origen por dos caminos distintos?
resumen depende de doble y de triple, y ambos dependen de base. Hay dos rutas desde base hasta resumen.
Cuando cambias base, un sistema reactivo mal diseñado podría recalcular resumen dos veces (una por cada ruta) o, peor, calcularlo con doble ya actualizado pero triple todavía viejo. Eso es un glitch: un estado intermedio que nunca debió existir.
El grafo de Angular es glitch-free. Ante un cambio en base, resumen se recalcula una sola vez, y cuando lo hace, tanto doble como triple ya están coherentes. Nunca observas la mezcla rara. El orden de evaluación del grafo (pull bajo demanda) junto con el versionado de cada nodo garantizan que un consumidor con varias rutas hacia el mismo origen converja en un único recálculo consistente.
Esto importa de verdad en producción. Es la diferencia entre una UI que parpadea con valores intermedios y una que actualiza limpio.
Versiones e igualdad: la poda que ahorra renders
Aquí entra el matiz que convierte el grafo en algo eficiente y no solo correcto.
Cada productor lleva, conceptualmente, una versión. Cuando un consumidor está sucio y va a recalcular, primero compara: “¿la versión de mis dependencias cambió de verdad respecto a la última vez que las usé?”. Si nada cambió realmente, no recomputa.
Y hay una segunda poda, más conocida: la función de igualdad. Por defecto, un signal usa Object.is para decidir si el nuevo valor es distinto del anterior. Si haces set con un valor igual al actual, el grafo no propaga nada aguas abajo.
const estado = signal('activo');
const etiqueta = computed(() => {
console.log('Recalculando etiqueta');
return estado().toUpperCase();
});
etiqueta(); // imprime "Recalculando etiqueta" → "ACTIVO"
estado.set('activo'); // mismo valor: Object.is da true → NO propaga
etiqueta(); // NO recalcula: el grafo nunca se marcó sucio
Puedes personalizar esa comparación cuando trabajas con objetos:
const usuario = signal(
{ id: 1, nombre: 'Ana' },
{ equal: (a, b) => a.id === b.id } // igual si el id no cambia
);
Ahora, si emites un objeto nuevo con el mismo id, el grafo lo considera igual y corta la propagación ahí mismo. Menos recálculos, menos renders. Esta equal es tu palanca para podar el grafo a mano cuando lo necesitas.
Effects y el scheduler: por qué no son síncronos
Un detalle que confunde: los effect()no corren en el instante exacto en que cambias un signal.
Cuando un signal del que depende un effect cambia, el effect se marca sucio y se agenda (scheduler). Angular lo ejecuta de forma agrupada, ligado normalmente a su ciclo de detección de cambios. Esto evita que un effect se dispare diez veces si haces diez set seguidos en la misma tarea: se ejecuta una vez, con el estado final.
const x = signal(0);
effect(() => console.log('x es', x()));
x.set(1);
x.set(2);
x.set(3);
// El effect NO imprime tres veces seguidas.
// Se agenda y corre una vez, con el valor final: "x es 3"
Si vienes de pensar en callbacks síncronos, este es el ajuste mental que necesitas. El effect reacciona, pero reacciona cuando toca, no a cada microcambio.
El contraste que lo explica todo: grafo vs. Zone.js
Ahora la pieza que da sentido a todo lo anterior.
Durante años, Angular detectó cambios con Zone.js + dirty checking. El modelo era de fuerza bruta: cuando algo podía haber cambiado (un click, un timeout, una respuesta HTTP), Angular recorría todo el árbol de componentes comprobando cada binding por si acaso. Funcionaba, pero el framework no sabía qué había cambiado. Solo sabía que algo pudo cambiar, y revisaba entero por si las moscas.
El grafo reactivo invierte el modelo. Angular ya no necesita preguntar “¿cambió algo en alguna parte?”. El propio grafo sabe exactamente qué signal cambió y qué nodos dependen de él. La actualización deja de ser una búsqueda y pasa a ser una notificación dirigida.
Zone.js + dirty checking
Grafo reactivo (Signals)
¿Qué sabe el framework?
Que algo pudo cambiar
Qué signal cambió exactamente
Alcance de la revisión
Todo el árbol de componentes
Solo el nodo y sus dependientes
Disparo
Cualquier evento async (click, timeout, HTTP)
El cambio concreto de un signal
Coste
Proporcional al tamaño del árbol
Proporcional a lo que de verdad cambió
Viabilidad zoneless
No (necesita Zone.js)
Sí (Angular puede prescindir de Zone.js)
Esto es la base técnica de zoneless —opción de producción desde Angular v20— y de la detección de cambios granular: si todo tu estado vive en signals, Angular puede prescindir de Zone.js por completo, porque el grafo ya le dice qué refrescar. Pasas de “revisa todo el árbol por si acaso” a “actualiza este nodo y sus tres dependientes, nada más”.
Si quieres ver dónde encaja esto en la versión actual, lo cuento en detalle en las novedades de Angular v22, y cómo este mismo grafo gobierna la carga de datos asíncrona en el post sobre la Resource API de Angular 22.
Qué puedes hacer con esto hoy
No necesitas memorizar internals para escribir signals. Pero con este modelo en la cabeza dejas de programar a ciegas:
Mete tu lógica derivada en computed() sin miedo a la performance: si nadie lo lee, no cuesta nada.
Deja de “optimizar” recálculos a mano — el grafo ya memoiza y poda por ti.
Usa equal personalizado cuando trabajes con objetos y veas renders de más.
Mueve estado de RxJS a signals donde la lógica sea síncrona y derivada; reserva RxJS para flujos de eventos reales.
La próxima vez que un computed() “no se ejecute cuando esperabas”, ya no vas a pensar que está roto. Vas a saber que el grafo está esperando, perezoso y eficiente, a que alguien lea el valor.
Si quieres dominar Signals con esta profundidad —el grafo, los effects, la migración desde RxJS y los patrones que aguantan en producción— eso es justo lo que construimos paso a paso en el curso de Angular Moderno. Y si quieres seguir afilando el modelo mental con la comunidad, te espero en Dominicode Labs.
Preguntas frecuentes
¿El grafo reactivo es lo mismo que los Signals?
No exactamente. Los Signals (signal, computed, effect) son las APIs que tú usas; el grafo reactivo es la estructura interna que Angular construye a partir de ellas para saber qué depende de qué. Tú escribes signals; Angular monta el grafo automáticamente por debajo.
¿Necesito entender el grafo reactivo para usar Signals?
Para escribir código que funcione, no. Para escribir código eficiente y entender por qué un computed() se comporta como lo hace —cuándo recalcula, cuándo no, por qué no parpadea— sí. Es la diferencia entre usar signals y dominarlos.
¿El grafo reactivo reemplaza a RxJS?
No lo reemplaza, lo complementa. El grafo de Signals brilla en estado síncrono y valores derivados. RxJS sigue siendo la mejor herramienta para flujos de eventos complejos, streams asíncronos y operadores como debounce o switchMap. Muchos proyectos usan ambos: signals para el estado, RxJS para los flujos.
¿Qué relación tiene con zoneless?
Total. El modo zoneless elimina Zone.js, y solo es viable porque el grafo reactivo ya le dice a Angular exactamente qué cambió y qué refrescar. Sin el grafo, Angular tendría que volver a revisar todo el árbol de componentes. El grafo es la condición que hace posible zoneless.
¿Un computed() se ejecuta siempre que cambian sus datos?
No. Es perezoso: se marca como “sucio” cuando cambia una dependencia, pero solo se recalcula de verdad cuando alguien lee su valor. Si nadie lo lee, no se ejecuta. Y una vez calculado, devuelve un valor memoizado hasta que cambie alguna dependencia.
¿Cómo evita Angular recalcular un valor dos veces ante un mismo cambio?
Gracias a la consistencia glitch-free y al versionado de nodos. Si un consumidor depende de un mismo origen por varias rutas, el grafo lo recalcula una sola vez y con valores coherentes, sin estados intermedios falsos ni recálculos duplicados.
Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.
Revisé hace poco un componente de Angular que cargaba una lista de productos. Contaba los observables con los dedos: un BehaviorSubject para la categoría seleccionada, un switchMap hacia HttpClient, un catchError para los fallos, un takeUntilDestroyed para no dejar suscripciones vivas, y al final, un async pipe en el template.
Todo correcto. Todo necesario. Y todo código que explica exactamente lo mismo que cualquier otro componente de carga de datos en la aplicación. La Resource API de Angular 22 resuelve exactamente este problema.
Ese patrón tiene quince años. Angular 22 tiene la respuesta definitiva.
Qué es la Resource API y por qué existe
La Resource API es el mecanismo nativo de Angular para gestionar operaciones asíncronas dentro del sistema de Signals. No es una librería de terceros, no es un wrapper sobre RxJS: es la pieza que faltaba para que el modelo reactivo de Angular estuviera completo.
La idea central es sencilla: tienes un signal que representa un parámetro (un ID, un filtro, una página), y quieres que Angular haga automáticamente el fetch cuando ese parámetro cambia. Sin subscribe, sin pipe, sin gestión manual del ciclo de vida.
En Angular 22 el ecosistema completo se compone de tres APIs:
resource() — fetch genérico con Promise. Estable en v22.
rxResource() — puente para servicios basados en Observable. Estable en v22.
httpResource() — wrapper declarativo sobre HttpClient. Experimental en v22.
El matiz de los estados de estabilidad importa. resource() y rxResource() ya son API pública con garantías de compatibilidad. httpResource() sigue marcado como experimental — la API puede cambiar. Para producción crítica, ten eso en cuenta.
resource(): el punto de entrada
Usa resource() cuando el origen de datos es una Promise o una función fetch directa. El parámetro reactivo se define en params, y la función que carga los datos en loader.
import { ChangeDetectionStrategy, Component, signal, resource } from '@angular/core';
Dos errores que verás en código antiguo o en tutoriales desactualizados:
El campo se llama params, no request. Eso era la API experimental anterior.
Los estados son strings literales: 'loading', 'reloading', 'error'… ResourceStatus no es un enum TypeScript nativo — es un objeto de constantes (as const), así que se compara con strings literales, no con ResourceStatus.Loading.
Cuando categoria cambia, Angular cancela el fetch anterior (usando el abortSignal que recibe el loader) y lanza uno nuevo. El ciclo de vida completo, gestionado sin escribir una sola línea de cleanup.
rxResource(): para servicios que devuelven Observables
La mayoría de proyectos Angular tienen servicios basados en HttpClient que devuelven Observable. Migrar todo a fetch puro no es viable ni deseable.
rxResource() es el puente. En lugar de loader, usa stream, que devuelve un Observable.
import { ChangeDetectionStrategy, Component, signal, inject } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
El import correcto es @angular/core/rxjs-interop, no @angular/core.
El método se llama stream, no loader. Los ejemplos de versiones experimentales anteriores usaban loader, de ahí la confusión.
httpResource(): la opción declarativa
httpResource() va un paso más allá: elimina la necesidad de declarar un servicio intermedio para casos de fetching simple. Lo declaras directamente en el componente.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { httpResource } from '@angular/common/http';
interface User { id: number; name: string; }
@Component({ selector: 'app-user-profile', changeDetection: ChangeDetectionStrategy.OnPush, template: ` @if (user.isLoading()) { <p>Cargando...</p> } @else if (user.error()) { <p>Error al cargar el perfil</p> } @else if (user.hasValue()) { <p>{{ user.value()?.name }}</p> } `, }) export class UserProfileComponent { userId = signal(1);
user = httpResource<User>(() => /api/user/${this.userId()}); }
httpResource() expone dos signals exclusivos que no tienen resource() ni rxResource():
.statusCode() — el código HTTP de la respuesta (200, 404, 500…)
.headers() — las cabeceras de la respuesta
Ambas son señales independientes de .status(), que sigue siendo el estado del ciclo de vida del recurso. Confundir .status() con .statusCode() es el error más frecuente al empezar con esta API.
Para respuestas que no son JSON:
// Texto plano
const readme = httpResource.text(() => /docs/readme.md);
Requiere provideHttpClient() en el bootstrap. Usa HttpClient e interceptores por debajo, así que tus interceptores de autenticación siguen funcionando sin cambiar nada.
Recuerda que httpResource() sigue marcado como experimental en v22. Para proyectos con requisitos estrictos de estabilidad de API, usa rxResource() con tu servicio HttpClient habitual hasta que se estabilice.
Cuándo usar cada uno
No es una decisión complicada si tienes claros los criterios:
| Situación | API recomendada | |—|—| | Fetch puro o API externa directa | resource() | | Tienes servicios con Observable existentes | rxResource() | | Fetching simple sin servicio intermedio (experimental) | httpResource() | | Necesitas el status code HTTP como signal | httpResource() |
Las tres APIs comparten la misma superficie de lectura: .value(), .isLoading(), .error(), .hasValue(), .status(), .reload(). Cambiar de una a otra es mínimamente invasivo.
Validación del dato en runtime
El genérico de TypeScript solo existe en tiempo de compilación. Si el backend devuelve algo inesperado, httpResource() no lanzará ningún error — simplemente tendrás un objeto mal tipado en runtime.
La opción parse existe exactamente para este caso:
Si el dato del backend no cumple el schema, el resource entra en estado 'error' automáticamente. Sin try/catch manual, sin runtime silencioso. Si quieres profundizar en cómo construir schemas robustos con Zod para este tipo de validación, el curso de Zod para TypeScript cubre exactamente estos patrones de producción.
Lo que cambia en tu arquitectura
La Resource API no elimina los servicios Angular — los reorganiza. Sigues necesitando servicios para encapsular lógica de negocio compleja, componer múltiples endpoints, o compartir estado entre componentes. Lo que elimina es el boilerplate de gestión de ciclo de vida en los componentes que simplemente cargan y muestran datos.
Un componente que antes necesitaba un servicio, tres operadores RxJS y un takeUntilDestroyed ahora expresa la misma intención en diez líneas. La lógica no desaparece — se mueve al lugar correcto.
Si quieres el cuadro completo — Signals, Resource API, Signal Forms, Zoneless y todo lo que llegó en v22 — el curso Angular Moderno tiene el módulo M10 dedicado íntegramente a Resource API con ejemplos sobre el proyecto ShopFlow.
Qué hacer hoy
Identifica en tu proyecto los componentes que tienen este patrón: signal o BehaviorSubject como parámetro, switchMap hacia HttpClient, y async pipe en el template.
Esos son tus candidatos para migrar a rxResource(). No necesitas reescribir los servicios. Solo cambias la forma en que el componente consume el Observable.
Empieza por un componente de solo lectura — uno que carga datos y no tiene formularios complejos. Comprueba que .status() en el template te da todo lo que necesitabas del loading$ que tenías antes.
Si funciona ahí, tienes el patrón. El resto de la migración es repetirlo.
Preguntas frecuentes
¿Cuál es la diferencia entre resource() y httpResource() en Angular 22?resource() acepta cualquier función que devuelva una Promise — puedes usarlo con fetch, con SDKs externos, o con cualquier operación asíncrona. httpResource() es un wrapper declarativo sobre HttpClient que además expone el status code HTTP y las cabeceras como signals independientes. La diferencia clave: httpResource() sigue siendo experimental en v22; resource() es API estable.
¿Puedo usar rxResource() si tengo servicios que devuelven Observables? Sí. rxResource() está diseñado exactamente para ese caso. En lugar de loader, defines un stream que devuelve un Observable. Tus servicios existentes no cambian — solo cambia cómo el componente los consume.
¿La Resource API reemplaza completamente RxJS en Angular? No. RxJS sigue siendo útil para transformaciones complejas de streams, operadores avanzados y casos donde necesitas combinar múltiples fuentes. La Resource API reemplaza el patrón subscribe/unsubscribe para carga de datos HTTP en componentes — no todos los casos de uso reactivo.
¿Qué ocurre con los datos en caché cuando cambia el signal de parámetros? Cuando el signal de params cambia, el resource entra en estado 'reloading' (no 'loading'). El valor anterior sigue disponible en .value() durante la recarga. Esto permite mostrar datos obsoletos mientras llegan los nuevos, en lugar de mostrar un spinner que vacía la UI. Es el comportamiento por defecto — no necesitas configurarlo.
¿Funciona la Resource API con Angular SSR? Sí. httpResource() usa HttpClient internamente, que ya tiene soporte de transferencia de estado para SSR. Con resource() y rxResource() necesitas gestionar tú mismo la transferencia de estado si el servidor precarga datos. La integración más limpia con SSR actualmente es a través de httpResource() o rxResource() con un servicio que use TransferState.
Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode. Ha migrado proyectos Angular en producción desde v2 hasta v22.