Apariencia
🏋️ Laboratorio 1 — Integración con una API (PokeAPI)
📅 2026-07-29 · 🗂️ Proyecto propio:
03-Proyectos/integracion-api-pokemon/(Angular 22, zoneless, Tailwind CSS) · 🎯 Primer laboratorio de autoestudio: armar de cero un buscador que consuma una API pública, sin depender de las capturas de una clase.A diferencia de las notas de
01-Clases/, esta no documenta lo que explicó el profe — es un proyecto propio, "jugando" con lo aprendido en la Clase 11 (servicios +HttpClient) para afianzarlo por mi cuenta.
🎯 Qué aprendí
- A repetir de cero el flujo completo de Clase 11 (servicio +
HttpClient+ signal zoneless) contra una API distinta, sin la guía paso a paso del profe. - Que cuando la API no devuelve el JSON con la forma exacta que necesito (a diferencia de la API de Clase 11), hace falta
.pipe(map(...))para transformar la respuesta cruda. - A manejar el caso de error de un
HttpClient(ej. un recurso que no existe,404), algo que Clase 11 no llegó a cubrir. - Que Angular 22 trae un decorador nuevo,
@Service(), como alternativa a@Injectable({ providedIn: 'root' }). - Que
ng newno acepta nombres con guion bajo, espacios ni tildes — solo minúsculas, números y guion medio. - A verificar el error real de la terminal antes de confiar en el subrayado del editor — mismo principio ya visto en Clase 11 ("Network antes que sospechar del código"), acá aplicado a errores de compilación en vez de errores de red.
🧩 Punto de partida: ¿por qué esta API y no la de Clase 11?
La Clase 11 usó una API de práctica (ejerciciostutorialesya.com/vue/datos.php) que devuelve el JSON ya con la forma exacta que necesita la interfaz (codigo, descripcion, precio). Para este laboratorio elegí la PokeAPI (https://pokeapi.co/api/v2/pokemon/{nombre}) a propósito: es pública, no necesita autenticación, pero devuelve un objeto enorme con muchísimos más campos de los que me interesan (abilities, stats, moves, held_items…). Esa diferencia obliga a un paso que Clase 11 no necesitó: transformar la respuesta cruda antes de guardarla.
🪜 Paso a paso
1. ng new — con un tropiezo en el nombre del proyecto
El primer intento de nombre de carpeta tenía espacios y tilde (Integración API Pokemon), y el segundo, guion bajo (integracion_api_pokemon). Ninguno de los dos sirve como nombre de proyecto de Angular:
⚠️
ng newsolo acepta nombres que empiecen con letra y contengan solo letras, números y guion medio (-). Ni espacios, ni tildes, ni guion bajo (_). La carpeta contenedora puede llamarse como sea — el nombre restringido es el que se le pasa al comando.
Nombre final usado: integracion-api-pokemon.
$ ng new integracion-api-pokemon
? Which stylesheet system would you like to use? Tailwind CSS
? Do you want to enable Server-Side Rendering (SSR)? No- Tailwind CSS — decisión consciente para este laboratorio, distinta a mis proyectos de clase (
app-05,app-semana04…) que usan CSS plano. Ver el paso 8 de estilos. - SSR — el primer intento quedó en "Yes" sin querer (se notó porque el proyecto tenía
server.ts,main.server.ts,provideClientHydration()yexpressenpackage.json). Para un laboratorio simple de solo-cliente, eso es complejidad de más → se borró el proyecto y se regeneró respondiendo No. - Confirmado sin
zone.jsenpackage.json: proyecto zoneless (default de Angular 22), igual queapp-05de Clase 11.
2. Activar HttpClient
Mismo patrón que Clase 11, en app.config.ts:
ts
import { provideHttpClient } from '@angular/common/http';
// ...
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes),
provideHttpClient(),
],3. Explorar la API con curl antes de tocar código
Mismo orden que Clase 11: confirmar qué devuelve la API de verdad antes de escribir la interfaz o el servicio. Filtrado con jq porque el JSON completo de la PokeAPI es gigante:
$ curl -s https://pokeapi.co/api/v2/pokemon/pikachu | jq '{id, name, height, weight, sprite: .sprites.front_default, tipos: [.types[].type.name]}'
{
"id": 25,
"name": "pikachu",
"height": 4,
"weight": 60,
"sprite": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/25.png",
"tipos": ["electric"]
}Esta forma filtrada (no la respuesta cruda completa) es la que define la interfaz IPokemon del paso siguiente.
4. Generar el servicio
$ ng generate service pokemon
CREATE src/app/pokemon.spec.ts
CREATE src/app/pokemon.ts📌 Novedad de Angular 22 — el decorador
@Service(): el schematic generó esto en vez del@Injectable({ providedIn: 'root' })de Clase 11:tsimport { Service } from '@angular/core'; @Service() export class Pokemon {}Verificado en el
.d.tsde@angular/core22.1.0:@Service()es un decorador nuevo, alternativa simplificada a@Injectable. Por defecto (autoProvidedimplícito entrue) se comporta igual queprovidedIn: 'root'— singleton global, listo parainject(). Con@Service({ autoProvided: false })se puede optar por registrarlo a mano en unproviders:, como el patrón viejo. Se mantuvo@Service()en este laboratorio (no se revirtió a@Injectable) porque es lo que generó el CLI estable del proyecto — ambos siguen siendo válidos en Angular 22.
5. Interfaz IPokemon y método del servicio
ts
// pokemon.ts
import { map, Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { Service, inject } from '@angular/core';
export interface IPokemon {
id: number;
name: string;
height: number;
weight: number;
sprite: string;
tipos: string[];
}
@Service()
export class Pokemon {
private http = inject(HttpClient);
private readonly apiUrl = 'https://pokeapi.co/api/v2/pokemon';
obtenerPokemon(nombre: string): Observable<IPokemon> {
return this.http.get<any>(`${this.apiUrl}/${nombre.toLowerCase()}`).pipe(
map((data) => ({
id: data.id,
name: data.name,
height: data.height,
weight: data.weight,
sprite: data.sprites.front_default,
tipos: data.types.map((t: any) => t.type.name),
})),
);
}
}http.get<any>(...)— la respuesta cruda se tipaanya propósito, porque su forma real (decenas de campos) no coincide conIPokemon. Recién dentro delmap(...)se arma el objeto con la forma que sí quiero usar en el resto de la app..pipe(map(...))— es la pieza que Clase 11 no necesitó (ahíhttp.get<IProducto[]>alcanzaba solo, porque la API ya devolvía la forma exacta).
6-7. Componente buscador-pokemon
bash
ng generate component buscador-pokemonts
// buscador-pokemon.ts
import { IPokemon, Pokemon } from './../pokemon';
import { Component, signal, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-buscador-pokemon',
imports: [FormsModule],
templateUrl: './buscador-pokemon.html',
styleUrl: './buscador-pokemon.css',
})
export class BuscadorPokemon {
private pokemonServide = inject(Pokemon);
nombreBuscado = '';
pokemon = signal<IPokemon | null>(null);
cargando = signal(false);
error = signal('');
buscar(): void {
if (!this.nombreBuscado.trim()) return;
this.cargando.set(true);
this.error.set('');
this.pokemon.set(null);
this.pokemonServide.obtenerPokemon(this.nombreBuscado).subscribe({
next: (datos) => {
this.pokemon.set(datos);
this.cargando.set(false);
},
error: () => {
this.error.set(`No se encontró el Pokémon "${this.nombreBuscado}"`);
this.cargando.set(false);
},
});
}
}pokemon,cargando,error— tres signals (obligatorio en zoneless, mismo motivo que Clase 11: solo.set()/.update()notifican a Angular que debe repintar).error: () => {...}dentro de.subscribe({...})— a diferencia de Clase 11 (que solo teníanext), acá la PokeAPI responde404si el nombre no existe. Sin este handler, un Pokémon inexistente rompería elObservableen silencio (sin avisar al usuario) en vez de mostrar un mensaje.
📝
pokemonServide(con "d" en vez de "c") quedó así por un typo al escribirpokemonService— se dejó tal cual porque se usa de forma consistente en todo el archivo y no afecta la compilación (es solo un nombre de variable).
html
<!-- buscador-pokemon.html -->
<div class="mx-auto mt-10 max-w-sm px-4">
<h2 class="mb-4 text-2xl font-bold text-gray-800">Buscador de Pokémon</h2>
<div class="flex gap-2">
<input
type="text"
placeholder="Ej: pikachu, charizard..."
[(ngModel)]="nombreBuscado"
(keyup.enter)="buscar()"
class="flex-1 rounded-lg border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none"
/>
<button
(click)="buscar()"
class="rounded-lg bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700"
>
Buscar
</button>
</div>
@if (cargando()) {
<p class="mt-4 text-gray-500">Cargando...</p>
}
@if (error()) {
<p class="mt-4 rounded-lg bg-red-50 px-3 py-2 text-red-600">{{ error() }}</p>
}
@if (pokemon(); as p) {
<div class="mt-4 rounded-xl border border-gray-200 p-4 text-center shadow-sm">
<img [src]="p.sprite" [alt]="p.name" class="mx-auto h-32 w-32" />
<h3 class="text-lg font-semibold capitalize text-gray-800">{{ p.name }}</h3>
<p class="text-sm text-gray-500">#{{ p.id }} — {{ p.height }} dm / {{ p.weight }} hg</p>
<p class="mt-2 text-sm font-bold text-blue-600 capitalize">
Tipo: {{ p.tipos.join(', ') }}
</p>
</div>
}
</div>[(ngModel)]necesitaFormsModuleen el arrayimportsdel componente — sin eso, Angular tira error de template ("can't bind to ngModel").capitalize(Tailwind) — la API devuelvename/tiposen minúscula (pikachu,electric); esta clase los muestra con mayúscula inicial sin tocar el dato crudo.
8. Conectar el componente en app.ts / app.html
ts
// app.ts
import { Component, signal } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { BuscadorPokemon } from './buscador-pokemon/buscador-pokemon';
@Component({
selector: 'app-root',
imports: [RouterOutlet, BuscadorPokemon],
templateUrl: './app.html',
styleUrl: './app.css',
})
export class App {
protected readonly title = signal('integracion-api-pokemon');
}html
<!-- app.html -->
<app-buscador-pokemon></app-buscador-pokemon>
<router-outlet />Se reemplazó todo el placeholder por defecto que trae ng new (logo de Angular, links a la documentación, etc.) por el componente propio.
9. Estilos con Tailwind CSS v4
ng new ya dejó todo configurado con el approach CSS-first de Tailwind v4 (sin tailwind.config.js):
css
/* src/styles.css */
@import 'tailwindcss';json
// .postcssrc.json
{ "plugins": { "@tailwindcss/postcss": {} } }Las clases de utilidad se usan directo en el HTML (ver el buscador-pokemon.html del paso 7): tarjeta centrada (mx-auto max-w-sm), input con foco resaltado (focus:border-blue-500 focus:outline-none), mensaje de error en rojo (bg-red-50 text-red-600).
10. Probar en el navegador
Búsqueda de "pikachu" → funcionó al primer intento tras corregir los bugs de abajo: se ve el sprite, #25 — 4 dm / 60 hg, Tipo: electric — coincide exactamente con el curl del paso 3.
🐛 Errores reales encontrados (y cómo se corrigieron)
A diferencia de una nota de clase (que documenta el código ya funcionando), acá quedan los errores tal cual pasaron, porque son la parte más útil para no repetirlos:
1. URL de la API duplicada. En un primer intento, apiUrl tenía el nombre de un Pokémon fijo (.../pokemon/pikachu) y el método le agregaba otro nombre al final → .../pokemon/pikachu/pikachu (404, no existe). Corregido dejando apiUrl como solo la base, sin nombre:
ts
// ❌ apiUrl con nombre fijo incluido
private readonly apiUrl = 'https://pokeapi.co/api/v2/pokemon/pikachu';
// ✅ apiUrl es solo la base
private readonly apiUrl = 'https://pokeapi.co/api/v2/pokemon';2. t.types.name en vez de t.type.name. Typo de plural/singular al mapear los tipos. El campo real de la API es type (confirmado con el curl del paso 3). El compilador no lo marcó porque la respuesta cruda estaba tipada any — hubiera tirado Cannot read properties of undefined recién en tiempo de ejecución.
3. .suscribe en vez de .subscribe. Este sí es error de compilación: "Property 'suscribe' does not exist on type 'Observable<IPokemon>'". Observable solo tiene .subscribe().
4. Un .subscribe({...}) pegado y anidado dos veces por accidente (copy-paste): el bloque de afuera llamaba obtenerPokemon(...) de nuevo adentro de su propio next, duplicando la llamada HTTP, y el bloque externo se quedó sin manejo de error. Se resolvió dejando un único subscribe({ next, error }).
5. El signal error nunca se mostraba en pantalla. Se actualizaba en el .ts (this.error.set(...)) pero faltaba el @if (error()) en el .html — el dato existía pero la vista no lo reflejaba (parecido en espíritu al bug zoneless de Clase 11: el dato está, la vista no se entera, salvo que acá la causa era simplemente que faltaba el @if, no un problema de reactividad).
6. Faltaba importar BuscadorPokemon en app.ts. El @Component({ imports: [...] }) del componente raíz solo tenía RouterOutlet — sin agregar el componente ahí, Angular no reconoce <app-buscador-pokemon> en el template ("'app-buscador-pokemon' is not a known element").
💡 Ninguno de estos bugs se encontró "adivinando" — se fueron resolviendo leyendo el mensaje de error real (de la terminal o del compilador) antes de tocar más código, mismo método que Clase 11 aplicó con la pestaña Network.
❓ Preguntas y respuestas
1. ¿Por qué acá hizo falta .pipe(map(...)) y en Clase 11 no?
Porque la API de Clase 11 devolvía el JSON ya con la forma exacta de
IProducto. La PokeAPI devuelve un objeto con muchos más campos de los que necesito —maptransforma esa respuesta cruda en la forma deIPokemonque sí quiero usar.
2. ¿Qué diferencia hay entre @Service() y @Injectable({ providedIn: 'root' })?
En Angular 22,
@Service()es un decorador nuevo y más simple: por defecto se comporta igual que@Injectable({ providedIn: 'root' })(singleton global, listo parainject()). Con@Service({ autoProvided: false })se puede optar por registrar el servicio a mano, como el patrón viejo.
3. ¿Por qué el .subscribe({...}) necesita un error: acá y en Clase 11 no era tan importante?
Porque la API de Clase 11 siempre respondía bien con datos fijos de práctica. La PokeAPI responde
404real si el nombre no existe — sinerror:, ese caso rompería elObservablesin avisar al usuario.
❓ Pendiente
- [ ] Confirmar en el navegador el caso de error (buscar un nombre que no existe, ej. "asdasd") y verificar que se ve el mensaje
No se encontró el Pokémon "...". - [ ] Capturar pantalla del resultado final con los estilos de Tailwind aplicados.
📎 Apuntes relacionados
- servicios-http.md — Clase 11, el mismo patrón base (servicio +
HttpClient+ signal zoneless) que este laboratorio reutiliza y extiende. 00-Notas/02-Conceptos.md(sección "Servicios, Inyección de Dependencias yHttpClient") — teoría general de@Injectable/providedIn: 'root'.
➡️ Siguiente
Verificar el caso de error en el navegador y, si se anima, sumar un segundo laboratorio con otro endpoint de la PokeAPI (ej. listar varios Pokémon con paginación) o probar un método HTTP distinto a GET.