httpResource vs TanStack Query: cuál usar en Angular 22
Un dev me escribió hace tres semanas con una captura de su package.json. Proyecto Angular 22 recién creado, dos días de vida, y ya tenía @tanstack/angular-query-experimental dentro.
Le pregunté por qué. Respuesta: "en React siempre lo uso".
Ese es el 80% de las discusiones de httpResource vs TanStack Query que veo por ahí: no son decisiones, son inercias. El resto viene del extremo contrario. Alguien leyó en un hilo que "httpResource no cachea", cerró la pestaña y descartó la API estándar de Angular sin llegar a preguntarse qué caché necesitaba de verdad su aplicación.
Las dos posturas fallan por el mismo motivo: comparan dos cosas que no juegan en la misma liga.
Este post no es un tutorial. Si buscas el cómo, ya publiqué la guía de la Resource API en Angular 22 y la de httpResource con señales. Aquí vamos a lo otro: qué eliges, por qué, y qué te va a costar.
httpResource vs TanStack Query: la respuesta corta
Usa httpResource por defecto en Angular 22. Es la API estándar, es estable desde la v22.0, no añade dependencias y hereda todo el ecosistema de HttpClient. Cambia a TanStack Query solo cuando necesites una caché de servidor compartida entre componentes con invalidación por clave, mutaciones optimistas o scroll infinito, y estés dispuesto a asumir que su adaptador de Angular se llama literalmente @tanstack/angular-query-experimental.
Esa es la decisión. El resto del post explica por qué, con la letra pequeña que casi nadie te cuenta.
La diferencia real: petición reactiva vs caché de estado de servidor
httpResource es una primitiva de petición reactiva. Le das una función que construye una URL a partir de señales, y la petición se vuelve a lanzar sola cuando esas señales cambian. Vive dentro del grafo reactivo de Angular como un nodo más. Su unidad de trabajo es una petición atada a un estado.
TanStack Query es otra cosa. Es una capa de caché de estado de servidor. Su unidad de trabajo no es la petición: es la clave. Cuando escribes queryKey: ['products', filtro], estás diciendo "estos datos existen en la aplicación bajo este identificador". A partir de ahí llegan la deduplicación de peticiones en vuelo, la invalidación cruzada, el stale-while-revalidate y las mutaciones optimistas. Todo eso son consecuencias de tener una clave y un almacén global, no de saber hacer un GET.
Por eso la pregunta "¿cuál es mejor?" lleva casi siempre a la respuesta equivocada. Es como comparar fetch con Redis. Uno trae datos; el otro decide quién los tiene, cuándo caducan y a quién hay que avisar.
La pregunta correcta es esta: ¿tu aplicación tiene un problema de caché de servidor, o solo tiene peticiones que dependen de un estado?
Cinco pantallas CRUD con filtros y un detalle no tienen problema de caché. Un dashboard donde ocho componentes distintos leen el mismo listado, donde una mutación en un modal tiene que refrescar tres widgets y donde el usuario espera ver el cambio antes de que responda el servidor, sí lo tiene.
El mismo caso resuelto con httpResource
Lista de productos con filtro reactivo. Esta es la versión con la API estándar.
import { Component, signal } from 39;@angular/core39;;
import { httpResource } from 39;@angular/common/http39;;
type Product = { id: string; name: string; price: number };
@Component({
selector: 39;app-products39;,
template: `
<input [value]="query()" (input)="query.set($any($event.target).value)" />
@if (products.isLoading()) {
<p>Cargando…</p>
} @else if (products.error()) {
<p>No se pudo cargar el catálogo</p>
} @else {
<ul>
@for (product of products.value(); track product.id) {
<li>{{ product.name }} — {{ product.price }} €</li>
}
</ul>
}
`,
})
export class ProductsComponent {
readonly query = signal(39;39;);
readonly products = httpResource<Product[]>(
() => `/api/products?q=${encodeURIComponent(this.query())}`,
{ defaultValue: [] },
);
}
Cero dependencias. Cero providers. El HttpResourceRef te expone value, status, error, headers, statusCode, progress e isLoading como señales, más los métodos hasValue() y reload().
Hay un detalle que se pasa por alto: la documentación oficial aclara que si hay una petición pendiente cuando cambia la dependencia, el resource cancela la anterior antes de lanzar la nueva. El caso del input que escribe rápido está resuelto de fábrica.
Si quieres validar la respuesta, tienes la opción parse con Zod o Valibot. Y como usa HttpClient por debajo, tus interceptors de auth, de reintentos y de logging siguen aplicando sin tocar nada. Este patrón, con la arquitectura de señales completa alrededor, es lo que trabajo a fondo en el curso de Angular Moderno.
El mismo caso resuelto con TanStack Query
Mismo componente, otra filosofía. Primero el provider en el arranque.
import { provideHttpClient } from 39;@angular/common/http39;;
import { provideTanStackQuery, QueryClient } from 39;@tanstack/angular-query-experimental39;;
bootstrapApplication(AppComponent, {
providers: [provideHttpClient(), provideTanStackQuery(new QueryClient())],
});
Y el componente:
import { Component, inject, signal } from 39;@angular/core39;;
import { HttpClient } from 39;@angular/common/http39;;
import { lastValueFrom } from 39;rxjs39;;
import {
injectQuery,
injectMutation,
QueryClient,
} from 39;@tanstack/angular-query-experimental39;;
type Product = { id: string; name: string; price: number };
type NewProduct = Omit<Product, 39;id39;>;
@Component({
selector: 39;app-products39;,
template: `
<input [value]="query()" (input)="query.set($any($event.target).value)" />
@if (products.isPending()) {
<p>Cargando…</p>
} @else if (products.isError()) {
<p>No se pudo cargar el catálogo</p>
} @else {
<ul>
@for (product of products.data() ?? []; track product.id) {
<li>{{ product.name }} — {{ product.price }} €</li>
}
</ul>
}
`,
})
export class ProductsComponent {
private readonly http = inject(HttpClient);
private readonly queryClient = inject(QueryClient);
readonly query = signal(39;39;);
readonly products = injectQuery(() => ({
queryKey: [39;products39;, this.query()],
queryFn: () =>
lastValueFrom(
this.http.get<Product[]>(`/api/products?q=${encodeURIComponent(this.query())}`),
),
}));
readonly createProduct = injectMutation(() => ({
mutationFn: (product: NewProduct) =>
lastValueFrom(this.http.post<Product>(39;/api/products39;, product)),
onSuccess: () => {
this.queryClient.invalidateQueries({ queryKey: [39;products39;] });
},
}));
}
Fíjate en lo que aparece y en lo que desaparece.
Aparece el queryKey: ese array es la identidad de los datos en toda la aplicación. Si otros seis componentes montan la misma clave, hay una sola petición y una sola copia en memoria. Y aparece invalidateQueries, la pieza que httpResource no tiene: un componente puede invalidar datos que él nunca pidió.
Desaparece la simplicidad. Tienes un provider global, un queryFn que devuelve promesas (de ahí el lastValueFrom para puentear el observable de HttpClient) y un vocabulario nuevo que todo el equipo tiene que aprender.
Tabla comparativa: httpResource vs TanStack Query en Angular 22
Solo celdas verificadas contra la documentación y los tipos publicados de ambos proyectos, a septiembre de 2026: Angular 22.1.4 y @tanstack/angular-query-experimental 5.102.8.
| Criterio | httpResource (Angular 22) | TanStack Query (adaptador Angular) |
|---|---|---|
| Estabilidad | Estable desde v22.0 | Experimental: breaking changes en releases minor y patch |
| Dependencias | Ninguna, viene en @angular/common/http |
@tanstack/angular-query-experimental, que arrastra @tanstack/query-core |
| Caché compartida entre componentes | No | Sí, por queryKey |
| Deduplicación de peticiones en vuelo | No entre instancias; cancela su propia petición anterior | Sí, por clave |
| Invalidación por clave | No | queryClient.invalidateQueries({ queryKey }) |
| Stale-while-revalidate | Conserva el valor anterior mientras recarga, pero no hay política de caducidad ni refetch en segundo plano | Sí, con staleTime y refetch en segundo plano |
| Reintentos automáticos | No incluidos (las opciones son parse, defaultValue, injector, equal y debugName) |
Sí, tres reintentos por defecto con backoff exponencial |
| Mutaciones y updates optimistas | Fuera de alcance por diseño | injectMutation con invalidación en onSuccess |
| Paginación infinita | A mano | injectInfiniteQuery |
| Devtools de datos | Sin panel de caché propio; los nodos aparecen en el signal graph de Angular DevTools vía debugName |
withDevtools() desde el subpath /devtools |
Interceptors de HttpClient |
Siempre, usa HttpClient por debajo |
Solo si el queryFn usa HttpClient |
| Testing | HttpTestingController estándar |
provideTanStackQuery en TestBed, QueryClient nuevo por spec, retry: false, y requiere Angular ≥ 19 |
| SSR / hydration | Entra en el transfer cache de provideClientHydration |
Sin guía de SSR propia para Angular; expone injectIsRestoring |
| Versión mínima de Angular | 22 para la versión estable | 16 (19 para la integración de testing) |
Dónde httpResource se queda corto de verdad
No voy a defender la API estándar más allá de lo que aguanta. Hay cuatro escenarios en los que httpResource te deja escribiendo infraestructura a mano.
Caché compartida entre componentes. Si el header, la sidebar y la tabla montan tres httpResource contra /api/user, son tres peticiones. No hay almacén común. Puedes subir el resource a un servicio con providedIn: 'root' y compartirlo, claro, pero eso ya es tu caché artesanal, con tus reglas de caducidad escritas por ti y mantenidas por ti.
Invalidación cruzada tras una mutación. Guardas un producto en un modal y necesitas refrescar el listado, el contador del carrito y el widget de novedades. Con httpResource toca llamar a reload() en cada uno, lo que obliga al modal a conocer a esos tres. Es acoplamiento, y escala mal.
Paginación infinita. Acumular páginas, saber si hay siguiente, mantener el scroll. injectInfiniteQuery te lo da resuelto. A mano son varias tardes y unos cuantos bugs sutiles.
Actualizaciones optimistas con rollback. Pintar el cambio antes de que responda el servidor y deshacerlo limpio si falla es un problema de gestión de estado, no de HTTP.
Y añado una limitación que no es un defecto sino una decisión de diseño: la documentación de Angular es explícita en que hay que evitar httpResource para mutaciones tipo POST o PUT, y usar directamente HttpClient. httpResource lee; no escribe.
Dónde TanStack Query te sale caro
Empecemos por lo que está escrito en su propia web, palabra por palabra:
"This library is currently in an experimental stage. This means that breaking changes will happen in minor AND patch releases."
Léelo otra vez, porque no es la típica etiqueta beta de adorno. Es el equipo de TanStack diciéndote que un patch puede romperte la capa de datos. Y no es un aviso antiguo: sigue ahí, en la documentación del adaptador de Angular, con el paquete en la versión 5.102.8. El "experimental" está en el nombre del paquete que vas a escribir en tu package.json.
En una prueba de concepto da igual. En una aplicación de empresa que va a vivir cinco años y que mantendrá otro equipo, eso significa fijar la versión, leerte cada changelog y aceptar que una parte crítica de tu arquitectura evoluciona a un ritmo que tú no controlas.
El segundo coste es más sutil: te sales del ecosistema de HttpClient. Los interceptors de Angular solo se aplican si tu queryFn usa HttpClient. En cuanto alguien del equipo escribe un queryFn con fetch porque le resulta más cómodo, esa petición se salta el interceptor de auth, el de reintentos y el de trazas. Y no falla en desarrollo: falla el día que caduca un token en producción.
Ese mismo salto te afecta al testing. httpResource se testea con HttpTestingController, igual que cualquier otra llamada de tu aplicación. Con TanStack Query montas provideTanStackQuery en el TestBed, creas un QueryClient limpio en cada spec, desactivas los reintentos para que los fallos no tarden tres backoffs y esperas a whenStable(). Se puede, está documentado, pero es una capa más que mantener. Si quieres el enfoque completo de testing en Angular moderno, lo tienes en el curso de Testing en Angular con Jest y Testing Library.
Y el tercer coste es el SSR. httpResource usa HttpClient, así que se beneficia del transfer cache de la hidratación sin que hagas nada. El adaptador de Angular de TanStack Query no publica hoy una guía de SSR equivalente a la de React.
Veredicto: el árbol de decisión
Me mojo.
Por defecto, httpResource. En un proyecto Angular 22 nuevo, empieza con la API estándar. Es estable, no amplía tu superficie de dependencias, integra con interceptors, testing y SSR, y cubre el 80% de los casos reales: listados, detalles, filtros, buscadores. Si hoy dudas, esta es tu respuesta.
TanStack Query cuando se cumplen las tres condiciones a la vez:
- Varios componentes independientes consumen los mismos datos de servidor y quieres una única fuente en memoria.
- Necesitas invalidación cruzada tras mutaciones, scroll infinito o updates optimistas, y ya has calculado lo que cuesta escribir todo eso a mano.
- Aceptas el riesgo de breaking changes en versiones patch y vas a fijar la versión exacta.
Si falla una sola de las tres, no lo metas.
Y la opción que casi nadie considera: los dos. No son excluyentes. Puedes servir el grueso de tus pantallas con httpResource y reservar TanStack Query para el módulo de dashboard donde de verdad existe un problema de caché compartida. Aísla la decisión en una zona del código en lugar de casarte con ella en toda la aplicación.
Lo que no vale es lo que hacía el dev del package.json: instalarlo el primer día porque en React lo usabas. Angular 22 no es React. El grafo de señales que llegó con la v22 ya te da reactividad de primera clase, y esa es justo la pieza que TanStack Query tuvo que inventar en el mundo de los hooks.
Qué hacer hoy
Abre tu proyecto y cuenta cuántos componentes distintos piden el mismo endpoint. Solo eso.
Si el número es cero o uno, no tienes un problema de caché de servidor: tienes peticiones reactivas, y httpResource te sobra para resolverlas. Si el número es tres o más, y además hay mutaciones que deben refrescar varias vistas a la vez, ahí sí te has ganado el derecho a meter una dependencia externa en la capa de datos.
Ese conteo tarda diez minutos y vale más que cualquier hilo de discusión en X.
En Dominicode Labs trabajamos este tipo de decisiones de arquitectura sobre proyectos reales, no sobre ejemplos de listas de tareas. Y si prefieres verlo en vídeo, en el canal de YouTube publico cada semana contenido de Angular moderno e IA aplicada al desarrollo.
Preguntas frecuentes
¿Qué es mejor en Angular 22, httpResource o TanStack Query?
httpResource es la mejor opción por defecto en Angular 22: es la API estándar, es estable desde la versión 22.0, no añade dependencias y hereda los interceptors, el testing y el transfer cache de HttpClient. TanStack Query solo compensa cuando varios componentes independientes consumen los mismos datos y necesitas invalidación por clave, mutaciones optimistas o scroll infinito. No resuelven el mismo problema: httpResource es una primitiva de petición reactiva y TanStack Query es una capa de caché de estado de servidor.
¿httpResource cachea las peticiones?
No, y ese malentendido origina la mitad de las discusiones sobre el tema. httpResource no mantiene un almacén de datos por clave: lanza la petición cuando cambian las señales de las que depende y expone el resultado como señal. Si necesitas que varios componentes compartan la misma copia en memoria, tienes que construir esa caché tú, por ejemplo elevando el resource a un servicio raíz, o usar una librería que ya la traiga resuelta.
¿Puedo usar httpResource y TanStack Query en la misma aplicación?
Sí, y en muchos proyectos es la decisión más sensata. Nada impide resolver el grueso de las pantallas con la API estándar de Angular y reservar TanStack Query para el módulo concreto que tiene un problema real de caché compartida e invalidación cruzada. Así limitas la superficie de la dependencia externa a una zona del código, en lugar de extenderla por toda la base.
¿Es seguro usar el adaptador de Angular de TanStack Query en producción?
Sí, siempre que fijes la versión exacta, revises el changelog antes de cada actualización y tengas tests que cubran la capa de datos. El riesgo está declarado en su propia documentación: habrá cambios que rompan en versiones minor y también en versiones patch. Si el proyecto lo va a mantener otro equipo dentro de dos años, piénsalo dos veces.
¿Los interceptors de HttpClient funcionan con TanStack Query?
Solo si la función de la query usa HttpClient por debajo. Si alguien la escribe con fetch o con axios porque le resulta más cómodo, esa petición se salta los interceptors de autenticación, reintentos y trazas de Angular, y el problema aparecerá en producción y no en desarrollo. Si adoptas la librería, conviene dejar por escrito en el equipo que toda petición pasa por HttpClient.
¿Sirve httpResource para POST y PUT?
No, y no es una limitación sino una decisión de diseño. La documentación de Angular recomienda de forma explícita evitar httpResource para mutaciones y usar directamente las APIs de HttpClient. httpResource está pensado para leer datos de forma reactiva; escribir es otro problema, con otro ciclo de vida y otros requisitos de control de errores.
¿Qué versión de Angular necesito para cada opción?
httpResource es estable desde Angular 22.0 y vive en el paquete de HTTP común, así que no tienes que instalar nada. El adaptador de TanStack Query declara compatibilidad desde Angular 16 en adelante, aunque su propia guía de testing avisa de que esa integración requiere Angular 19 o superior, porque las versiones anteriores no soportan PendingTasks.
Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.
¿Te resultó útil este artículo?
Compártelo con tu comunidad y ayuda a otros desarrolladores.
