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. Lanza una excepción si el recurso está en error. Pintar la lista con @for, después de comprobar hasValue()
.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 -->
@if (filmService.filmsResource.hasValue()) {
@for (film of filmService.filmsResource.value(); track film.imdbID) {
<div class="card">{{ film.title }}</div>
}
}

value() devuelve undefined mientras la petición carga, pero si la petición falló, lanza una excepción en vez de devolver undefined. Con el servidor respondiendo 500, una lista que leía value() dentro de un @for rompió la vista y el mensaje de error de error() nunca llegó a mostrarse.

Por eso, antes de leer el valor se pregunta si lo hay con hasValue(), que es true solo cuando los datos ya llegaron:

Estado del recurso isLoading() error() hasValue() value()
Cargando true undefined false undefined
Con datos false undefined true Los datos
Con error false El error false Lanza una excepción

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.

Hasta ahora la URL de httpResource era un texto fijo. Cuando debe construirse con un dato que puede cambiar, por ejemplo el id de la película que se quiere ver, se le pasa una función que devuelve la URL:

readonly id = signal('tt0111161');
readonly film = httpResource<Film>(() => `api/films/${this.id()}`);

La función lee el signal id, así que Angular la vuelve a ejecutar cada vez que id cambia, y el httpResource hace una petición nueva con la URL nueva, sin código adicional. Al pasar de /artistas/1 a /artistas/2, el recurso pidió /artistas/2 y la vista se actualizó.

La función también puede devolver undefined, y entonces el recurso no pide nada:

readonly film = httpResource<Film>(() => {
const id = this.id();
return id ? `api/films/${id}` : undefined;
});

Mientras la función devuelve undefined, no sale ninguna petición y el recurso no tiene valor ni error. Es útil cuando el dato que arma la URL todavía no existe, o cuando otra fuente ya trae la información y no hace falta pedirla.

La regla de oro de este tema dice que solo el servicio habla con la red. Para respetarla, el servicio ofrece un método que crea el recurso, y el componente lo llama al inicializarse:

film.service.ts
filmResource(id: () => string | undefined): HttpResourceRef<Film | undefined> {
return httpResource<Film>(() => {
const valor = id();
return valor ? `${this.apiUrl}/${valor}` : undefined;
});
}
film-detail.ts
readonly id = input<string>();
readonly recurso = inject(FilmService).filmResource(() => this.id());

Esto funciona: httpResource necesita crearse en un contexto de inyección, y la inicialización de un campo del componente lo es, aunque la llamada pase por un método del servicio. El componente le entrega al servicio una función que lee su id, y el servicio devuelve el recurso listo. El detalle completo, con la ruta que entrega el id, está en Enrutamiento y Angular Router.

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 { Observable } from 'rxjs';
import { Film } from './film';
@Injectable({ providedIn: 'root' })
export class FilmService {
// 1. Inyecta HttpClient, igual que en la sesión 2 de R1
private http: HttpClient = inject(HttpClient);
private apiUrl: string = 'https://mi-api.com/films';
// 2. El método devuelve un Observable, no ejecuta nada todavía
createFilm(newFilm: Film): Observable<Film> {
return this.http.post<Film>(this.apiUrl, newFilm);
}
}
film-form.component.ts
import { Component, inject } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { FilmService } from './film.service';
import { Film } from './film';
export class FilmFormComponent {
private filmService: FilmService = inject(FilmService);
onSubmit(formData: Film): void {
// 3. subscribe() es quien activa la petición HTTP
this.filmService.createFilm(formData).subscribe({
next: (creada: Film) => {
// 4a. Éxito: creada es un Film real, con sus propios atributos.
// Por eso puede leer creada.imdbID: no es un objeto genérico.
console.log('Película creada, imdbID asignado:', creada.imdbID);
},
error: (err: HttpErrorResponse) => {
// 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í…

Cuando una petición falla, tanto HttpClient (en el error del subscribe) como httpResource (en error()) entregan un HttpErrorResponse. Tiene dos campos útiles:

  • status, el código HTTP de la respuesta.
  • error, el cuerpo de la respuesta ya convertido de JSON. Si el back es el de este curso, trae un objeto con status, message y timestamp, y message es el texto que escribió quien programó la regla.
error: (err: HttpErrorResponse) => {
if (err.status === 0) {
this.mensaje.set('No se pudo contactar al servidor.');
} else {
this.mensaje.set(err.error?.message ?? 'Ocurrió un error.');
}
}
status Qué significa Qué mostrar
0 La petición no obtuvo una respuesta utilizable: el servidor está caído o el navegador bloqueó la respuesta por CORS Un mensaje general; no hay cuerpo que leer
400 El back rechazó la operación por una regla de negocio err.error.message
404 El recurso que se pidió no existe err.error.message, o un texto propio

Una respuesta 400 con el cuerpo { "status": 400, "message": "El artista ya tiene ese álbum", ... } llegó con status igual a 400 y err.error.message con ese texto, tanto por HttpClient como por httpResource. Contra un servidor que no declara CORS para el origen de la aplicación, el error llegó con status igual a 0.

No todo POST envía datos. Cuando la operación consiste en relacionar dos recursos que ya existen (por ejemplo, asociar un actor a una película) los dos identificadores ya viajan en la URL y no hay nada que mandar en el cuerpo:

asociarActor(filmId: string, actorId: string): Observable<Film> {
return this.http.post<Film>(`${this.apiUrl}/${filmId}/actors/${actorId}`, null);
}
  • El segundo argumento es null. La petición sale sin cuerpo: sin Content-Type y con cero bytes.
  • La respuesta sí trae datos. El back suele devolver el recurso ya actualizado (aquí, la película con su lista de actores), y por eso el genérico <Film> sigue siendo necesario.
  • Se suscribe igual. Es un Observable frío: sin .subscribe() la petición no sale.

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): Observable<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): Observable<void> {
return this.http.delete<void>(`${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.

film.service.ts, el patrón completo
import { Injectable, inject } from '@angular/core';
import { HttpClient, httpResource } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Film } from './film';
@Injectable({ providedIn: 'root' })
export class FilmService {
private http: HttpClient = inject(HttpClient);
private apiUrl: string = 'api/films';
// La lista, siempre reactiva (GET)
filmsResource = httpResource<Film[]>(this.apiUrl);
// En la plantilla:
// filmsResource.value() → los datos
// filmsResource.isLoading() → spinner
// filmsResource.error() → mensaje
// Crear
create(f: Film): Observable<Film> {
return this.http.post<Film>(this.apiUrl, f);
}
// Editar
update(id: string, f: Film): Observable<Film> {
return this.http.put<Film>(`${this.apiUrl}/${id}`, f);
}
// Borrar
remove(id: string): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${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): void {
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: HttpErrorResponse) => 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?
7. Un recurso de httpResource quedó en estado de error. ¿Qué ocurre si la plantilla llama a value()?
8. Un httpResource usa la función () => { const id = this.id(); return id ? `api/films/${id}` : undefined; }. Mientras id() es undefined, ¿qué hace?
9. Una petición falla en el navegador con HttpErrorResponse.status igual a 0, aunque el mismo endpoint responde bien en Postman. ¿Qué es lo más probable?
10. Para asociar un actor ya existente a una película con POST /films/{filmId}/actors/{actorId}, ¿qué se pasa como segundo argumento de http.post()?