Tutorial: listar datos remotos en Cards con httpResource
En este taller vamos a construir, paso a paso, una app Angular standalone que trae un catálogo de películas desde un Gist público de GitHub y lo muestra en tarjetas (Cards).
1. Objetivos de aprendizaje
Sección titulada «1. Objetivos de aprendizaje»Al terminar este taller vas a ser capaz de:
- Crear una aplicación Angular standalone, sin
NgModule. - Explicar qué es un environment y por qué la URL de una API no debería vivir hardcodeada dentro de un componente.
- Registrar
HttpClienta nivel de aplicación conprovideHttpClient(). - Crear un componente standalone enfocado en una sola responsabilidad (mostrar un catálogo).
- Hacer fetch de datos remotos con
httpResource(), la API reactiva basada en signals, sin escribirsubscribe()/unsubscribe()a mano. - Manejar los tres estados de una petición (cargando, error, datos) y
renderizarlos en el template con la sintaxis
@if/@for.
Prerrequisitos: Node.js y Angular CLI instalados, y conocimientos básicos de TypeScript (interfaces, tipos, clases).
2. Crear la aplicación Angular (standalone)
Sección titulada «2. Crear la aplicación Angular (standalone)»Desde v17, Angular CLI genera proyectos standalone por defecto (sin
AppModule). Crea el proyecto así:
ng new filmsignals2 --style=css --routing=false --ssr=falsecd filmsignals2--routing=false: no necesitamos rutas para este ejercicio.--ssr=false: evita que el CLI configure Server-Side Rendering, que no usaremos aquí. Si el CLI te pregunta por SSR/IA en el asistente interactivo, responde “No”.
Revisa la estructura generada:
src/├── index.html├── main.ts└── app/ ├── app.component.ts ├── app.component.html └── app.config.tsapp.component.html es el template que el CLI genera por defecto para
AppComponent. Más adelante lo vamos a reemplazar por un template inline
(no necesita HTML propio), así que puedes borrarlo cuando lleguemos ahí.
Dos archivos clave:
src/main.ts — el punto de entrada. Se hace bootstrap directo del componente raíz:
import { bootstrapApplication } from '@angular/platform-browser';import { appConfig } from './app/app.config';import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig) .catch(err => console.error(err));src/app/app.component.ts — el componente raíz declara sus dependencias
en imports:
@Component({ selector: 'app-root', standalone: true, imports: [], template: `Hola`, // lo reemplazaremos en el paso 5})export class AppComponent {}3. Crear un environment
Sección titulada «3. Crear un environment»Antes de tocar el componente, separamos la URL del Gist del código: así el componente no sabe “de dónde” vienen los datos, solo que existe una URL configurada en algún lado. Esto también permite tener valores distintos por entorno (desarrollo, producción) sin tocar el componente.
Creamos la carpeta environments/:
src/environments/environment.ts (usado en producción por defecto):
Cuando se tenga la URL del Gist, se reemplaza el valor acá en los environments.
export const environment = { production: true, gistUrl: 'https://gist.githubusercontent.com/<tu-usuario>/<id>/raw/.../films.json',};src/environments/environment.development.ts (para ng serve):
export const environment = { production: false, gistUrl: 'https://gist.githubusercontent.com/<tu-usuario>/<id>/raw/.../films.json',};💡 Cada estudiante puede crear su propio Gist con su propio JSON de películas (o cualquier otro dataset) y pegar aquí su URL.
Para que Angular sepa que debe usar environment.development.ts cuando
corres en modo desarrollo, registra un fileReplacements en angular.json,
dentro de la configuración development del target build:
"development": { "optimization": false, "extractLicenses": false, "sourceMap": true, "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.development.ts" } ]}Con esto, en cualquier parte del código puedes hacer
import { environment } from '../../environments/environment'; y el
bundler resuelve el archivo correcto según la configuración de build.
4. Incluir HttpClient
Sección titulada «4. Incluir HttpClient»httpResource() (que usaremos en el paso 6) necesita que HttpClient esté
registrado en el árbol de inyección de la aplicación. En un proyecto
standalone eso se hace con un provider function:
src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';import { provideHttpClient } from '@angular/common/http';
export const appConfig: ApplicationConfig = { providers: [ provideHttpClient(), ],};5. Crear el componente
Sección titulada «5. Crear el componente»Genera el componente que va a mostrar el catálogo:
ng generate component film-listEsto crea src/app/film-list/ con su .ts, .html y .css, ya marcado
como standalone: true por defecto.
Antes de programar el fetch, define el modelo de datos — el contrato que describe la forma de cada elemento que viene del Gist:
src/app/film.model.ts
export interface Film { title: string; year: string; director: string; imdbRating: string; imdbID: string; poster: string;}Y conecta el componente en la raíz de la app:
src/app/app.component.ts
import { Component } from '@angular/core';import { FilmListComponent } from './film-list/film-list.component';
@Component({ selector: 'app-root', standalone: true, imports: [FilmListComponent], template: `<app-film-list />`,})export class AppComponent {}AppComponent no tiene lógica propia: su único trabajo es renderizar
FilmListComponent. Toda la responsabilidad del catálogo vive en un solo
lugar.
6. Fetch de los datos con HttpResource
Sección titulada «6. Fetch de los datos con HttpResource»Ahora sí, el corazón del taller. httpResource() es una API (en developer
preview) de @angular/common/http que envuelve un GET de HttpClient y
devuelve un recurso reactivo basado en signals: no te suscribes a nada,
simplemente lees sus signals en el template.
src/app/film-list/film-list.component.ts
import { Component } from '@angular/core';import { httpResource } from '@angular/common/http';import { Film } from '../film.model';import { environment } from '../../environments/environment';
@Component({ selector: 'app-film-list', standalone: true, templateUrl: './film-list.component.html', styleUrl: './film-list.component.css',})export class FilmListComponent {
filmsResource = httpResource<Film[]>(() => environment.gistUrl);}filmsResource expone tres signals que usaremos en el template:
| Signal | Qué significa |
|---|---|
filmsResource.isLoading() |
true mientras la petición está en curso |
filmsResource.error() |
Contiene el error si la petición falló |
filmsResource.value() |
El Film[] recibido, una vez la petición resuelve |
💡 Con
httpResource(), todo el manejo de estado (carga, error, datos) viene resuelto por el propio recurso.
⚠️
httpResource()está en developer preview: es perfecta para aprender el patrón de signals + datos remotos, pero su API todavía puede cambiar en futuras versiones de Angular.
7. El template
Sección titulada «7. El template»src/app/film-list/film-list.component.html
Lo construimos por partes. Cada bloque retoma la numeración justo donde terminó el anterior, así que las líneas que ves corresponden a su posición real en el archivo final.
Primero, el contenedor y el encabezado — Bootstrap puro, todavía sin nada de Angular:
<div class="container py-4">
<h2 class="mb-4">Catálogo de Películas</h2>Después vienen los dos primeros estados de la petición: cargando y error.
@if (filmsResource.isLoading()) { <p class="text-muted">Cargando películas…</p> }
@if (filmsResource.error()) { <div class="alert alert-danger"> No se pudo cargar el catálogo. Verifica la conexión. </div> }@if es independiente por bloque: los tres @if de este template
(líneas 5, 9 y 15) no son mutuamente excluyentes en el código — no hay un
@else que los conecte — pero en la práctica solo uno tiene contenido a la
vez, porque isLoading(), error() y value() no coexisten con datos
reales al mismo tiempo.
El tercer estado, cuando la petición ya trajo datos, abre un @for sobre el
arreglo de películas:
@if (filmsResource.value()) { <div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 g-4"> @for (film of filmsResource.value(); track film.imdbID) {track film.imdbID es obligatorio en @for. Angular lo usa para saber
qué elemento del DOM corresponde a qué elemento de la lista cuando esta
cambia. Una buena clave de track es un identificador único y estable —
nunca el índice si los datos se pueden reordenar o filtrar.
Dentro del @for, cada película se dibuja como una Card de Bootstrap:
<div class="col"> <div class="card h-100 shadow-sm film-card"> <img class="card-img-top" [src]="film.poster.replace('http://', 'https://')" [alt]="film.title" style="height: 260px; object-fit: cover;" /> <div class="card-body"> <h5 class="card-title">{{ film.title }}</h5> <p class="card-text text-muted small"> {{ film.year }} · {{ film.director }} </p> <span class="badge bg-warning text-dark">★ {{ film.imdbRating }}</span> </div> </div> </div>- La normalización del poster (línea 22,
film.poster.replace('http://', 'https://')) ocurre inline en el template: si la página corre sobre HTTPS, cargar una imagen HTTP el navegador la bloquea (contenido mixto). - Las clases
card,row,badge, etc. son de Bootstrap, cargado por CDN ensrc/index.html— no es una dependencia npm en este ejemplo.
Y para terminar, las tres estructuras se cierran en el orden inverso al que
se abrieron — @for, el <div class="row"> y el @if de value() — antes
de cerrar el contenedor:
} </div> }
</div>Con esto, al correr ng serve deberías ver el catálogo cargando, y luego
las tarjetas con póster, título, año, director y rating de cada película.