Ir al contenido

HttpResource y HttpClient

Dos herramientas, un rol cada una. Una le ofrece el estado reactivo, la otra le da control total sobre las acciones. Saber cuándo usar cada cual es lo que separa un componente que funciona de uno que escala.

Para leer (GET), httpResource

Carga datos del servidor y le entrega value(), isLoading() y error() listos para usar en la plantilla.

Para actuar (POST / PUT / DELETE), HttpClient

Ejecuta cualquier método HTTP. Usted maneja cuándo se dispara y qué pasa con la respuesta. Imprescindible para crear, editar y borrar.

Ya tiene sus interfaces TypeScript, ya tiene un arreglo de prueba en el componente. Pero la realidad es que los datos viven en un servidor (un Gist de GitHub hoy, un back en Spring más adelante). Hay que ir a buscarlos.

Sin Angular, haría algo así con el API nativa del navegador:

sin-framework.ts
// Usted gestiona TODO: carga, error, datos
let films = [];
let isLoading = false;
let error = null;
fetch('data/films.json')
.then(r => r.json())
.then(data => { films = data; isLoading = false; })
.catch(err => { error = err; isLoading = false; });
// Problema: ¿cómo aviso a la vista que cambió el estado?

La estructura de datos que se usa en todos los ejemplos es la del archivo films.json del proyecto FilmSignals. Así se ve un elemento:

films.json (fragmento)
{
"title": "Interstellar",
"year": "2014",
"director": "Christopher Nolan",
"imdbRating": "8.6",
"imdbID": "tt0816692",
"poster": "https://..."
}

Y la interfaz TypeScript que lo modela, el contrato que se firma con el compilador:

film.ts
export interface Film {
title: string;
year: string;
director: string;
imdbRating: string;
imdbID: string;
poster: string;
}

httpResource es una función de Angular (disponible desde v19) que hace el GET y le entrega el resultado ya empaquetado en signals. No hay subscribe, no hay manejo manual de estados.

Esto es todo lo que necesita para traer la lista de películas y conectarla a su plantilla:

film.service.ts
import { Injectable, inject } from '@angular/core';
import { httpResource } from '@angular/common/http';
import { Film } from './film';
@Injectable({ providedIn: 'root' })
export class FilmService {
// Una sola línea: URL → signals listas para usar
filmsResource = httpResource<Film[]>('data/films.json');
}
flowchart LR
    call["httpResource&lt;Film[]&gt;('data/films.json')"] --> obj

    subgraph obj["objeto que devuelve: filmsResource"]
        direction TB
        v["value()<br/>Film[] | undefined"]
        l["isLoading()<br/>boolean"]
        e["error()<br/>unknown | undefined"]
    end

httpResource devuelve un objeto con varios signals. Los tres que usará siempre en la plantilla son:

Signal Tipo ¿Qué contiene? Cuándo usarlo
.value() Film[] | undefined Los datos cuando ya llegaron. undefined mientras carga. Pintar la lista con @for
.isLoading() boolean true mientras la petición está en vuelo. Mostrar un spinner o texto “Cargando…”
.error() unknown | undefined El error si la petición falló. undefined en caso de éxito. Mostrar un mensaje de error con @if
film-list.component.html
<!-- Mientras carga -->
@if (filmService.filmsResource.isLoading()) {
<p>Cargando películas…</p>
}
<!-- Si hubo error -->
@if (filmService.filmsResource.error()) {
<p class="error">Error al cargar. Intente de nuevo.</p>
}
<!-- Los datos cuando llegan -->
@for (film of filmService.filmsResource.value() ?? []; track film.imdbID) {
<div class="card">{{ film.title }}</div>
}

Demo interactiva: los tres estados en acción

Sección titulada «Demo interactiva: los tres estados en acción»

Simule lo que Angular haría al ejecutar httpResource('data/films.json'):

isLoading(): false
value(): undefined
error(): undefined
Pulse un botón para ver cómo cambian los signals.

httpResource es perfecta para leer. Pero si el usuario quiere crear una película nueva, editarla o borrarla, necesita enviar datos al servidor, y para eso existe HttpClient.

GET, httpResource

Leer la lista completa. Angular gestiona carga y error automáticamente.

POST, HttpClient

Enviar un nuevo objeto al servidor. Usted define qué pasa al recibir la respuesta.

PUT, HttpClient

Reemplazar un objeto existente por su id.

DELETE, HttpClient

Eliminar un recurso del servidor por su id.

film.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Film } from './film';
@Injectable({ providedIn: 'root' })
export class FilmService {
// 1. Inyecta HttpClient, igual que en la sesión 2 de R1
private http = inject(HttpClient);
private apiUrl = 'https://mi-api.com/films';
// 2. El método devuelve un Observable, no ejecuta nada todavía
createFilm(newFilm: Film) {
return this.http.post<Film>(this.apiUrl, newFilm);
}
}
film-form.component.ts
export class FilmFormComponent {
private filmService = inject(FilmService);
onSubmit(formData: Film) {
// 3. subscribe() es quien activa la petición HTTP
this.filmService.createFilm(formData).subscribe({
next: (creada) => {
// 4a. Éxito: la API devolvió la película creada
console.log('Película creada:', creada);
},
error: (err) => {
// 4b. Error: algo salió mal en el servidor
console.error('Error al crear:', err);
}
});
}
}
flujo mental
Usuario hace clic en "Guardar"
Componente llama filmService.createFilm(datos)
Servicio llama http.post(url, datos) ← Observable creado, no disparado
Componente llama .subscribe() ← Aquí se envía la petición
Servidor procesa y responde
next() o error() se ejecutan
Componente actualiza el estado (signal) → vista reacciona

Rellene los campos y observe qué JSON se enviaría al servidor y qué respuesta llegaría:

{
  "title": "",
  "year": "",
  "director": "",
  "imdbRating": ""
}
La respuesta del servidor aparecerá aquí…

PUT y DELETE: la misma idea, distinto método

Sección titulada «PUT y DELETE: la misma idea, distinto método»
Ver snippet de PUT
// Reemplaza la película con ese imdbID
updateFilm(id: string, updated: Film) {
return this.http.put<Film>(`${this.apiUrl}/${id}`, updated);
}
Ver snippet de DELETE
// Borra la película, generalmente no devuelve cuerpo
deleteFilm(id: string) {
return this.http.delete(`${this.apiUrl}/${id}`);
}
// En el componente, igual: .subscribe({ next: ..., error: ... })

En la práctica, un servicio Angular que maneja una entidad usa los dos: httpResource para la lista (siempre actualizada) y HttpClient para las operaciones que modifican datos.

httpResource, leer
// La lista, siempre reactiva
filmsResource =
httpResource<Film[]>('api/films');
// En la plantilla:
// films.value() → los datos
// films.isLoading() → spinner
// films.error() → mensaje
HttpClient, actuar
// Crear
create(f: Film) {
return http.post('api/films', f);
}
// Editar
update(id, f: Film) {
return http.put(`api/films/${id}`, f);
}
// Borrar
remove(id) {
return http.delete(`api/films/${id}`);
}

Refrescar un httpResource después de escribir, .reload()

Sección titulada «Refrescar un httpResource después de escribir, .reload()»

Después de crear, editar o borrar algo con HttpClient, la lista que trajo httpResource queda desactualizada: sigue mostrando lo que había antes de la escritura. httpResource resuelve esto con un método propio, .reload(), que vuelve a disparar el mismo GET y actualiza value() con la respuesta nueva.

film-form.component.ts
onSubmit(formData: Film) {
this.filmService.createFilm(formData).subscribe({
next: () => {
// El POST ya terminó: la lista que muestra otro componente
// sigue mostrando los datos viejos hasta que se refresque.
this.filmService.filmsResource.reload();
},
error: (err) => console.error('Error al crear:', err),
});
}
regla de oro
// ✓ El servicio es el único que habla con la red
// ✓ El componente llama métodos del servicio
// ✗ NUNCA inyecte HttpClient directamente en un componente
// La presentación no sabe de dónde vienen los datos.
// Si mañana cambia el back, solo se toca el servicio.
1. Quiere mostrar la lista de películas en su componente y que se pinte automáticamente cuando lleguen los datos. ¿Cuál es la herramienta más adecuada?
2. Un estudiante escribe this.http.post('api/films', newFilm) en su servicio y nada ocurre en la red. ¿Qué falta para que la petición HTTP se ejecute?
3. ¿Cuál de estos signals NO proporciona httpResource automáticamente?
4. En FilmService del proyecto FilmSignals, los signals internos son private (por ejemplo filmsListSignal) y el servicio expone getters públicos para leerlos. ¿Por qué?
5. Su componente necesita borrar una película cuando el usuario pulsa un botón. ¿Cuál es el flujo correcto?
6. Un servicio providedIn: 'root' tiene un solo filmsResource. Dos componentes distintos lo inyectan. Uno de ellos crea una película y llama filmsResource.reload(). ¿Qué pasa con el otro componente?