Para leer (GET), httpResource
Carga datos del servidor y le entrega value(), isLoading() y error() listos para
usar en la plantilla.
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:
// Usted gestiona TODO: carga, error, datoslet 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:
{ "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:
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:
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<Film[]>('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 |
<!-- 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> }}hasValue()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 |
Simule lo que Angular haría al ejecutar httpResource('data/films.json'):
isLoading(): falsevalue(): undefinederror(): undefinedHasta 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:
filmResource(id: () => string | undefined): HttpResourceRef<Film | undefined> { return httpResource<Film>(() => { const valor = id(); return valor ? `${this.apiUrl}/${valor}` : undefined; });}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.
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); }}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); } }); }}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 reaccionaRellene 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);}null. La petición sale sin cuerpo: sin Content-Type y con cero bytes.<Film> sigue siendo necesario..subscribe() la petición no sale.// Reemplaza la película con ese imdbIDupdateFilm(id: string, updated: Film): Observable<Film> { return this.http.put<Film>(`${this.apiUrl}/${id}`, updated);}// Borra la película, generalmente no devuelve cuerpodeleteFilm(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.
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}`); }}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.
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), });}// ✓ 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.