Apariencia
🧰 Clase 11 — Servicios (proyecto app-05)
📅 2026-07-25 · 🗂️ Proyecto:
app-05(destino en este repo:02-Ejercicios/app-05) · 🎯 Servicios en Angular (@Injectable) — tema todavía en desarrollo en clase.⬅️ Viene después de Clase 10 (Formularios), mismo día de clase — el profe pasó a un proyecto nuevo (
app-05) para el siguiente tema.
🎯 Qué aprendí
- Qué es un servicio (
@Injectable({ providedIn: 'root' })) y cómo consumirlo desde un componente coninject(). - Cómo activar
HttpClientpara toda la app conprovideHttpClient(). - El flujo completo de conectar un servicio a una API real: interfaz tipada,
.get<T>(), y.subscribe()para disparar la petición. - Un bug real de zoneless (HTTP responde 200 pero la vista no se actualiza) y cómo diagnosticarlo sin adivinar: Network antes que sospechar del código.
🗺️ Diagrama: arquitectura del servicio

ListaProductosno llama aHttpClientdirecto — pide el dato aProductoservice, que es quien conoce la URL y arma elObservable.HttpClientsolo existe porqueprovideHttpClient()lo activó enapp.config.ts— sin esa línea,inject(HttpClient)fallaría en tiempo de ejecución.- La API externa (borde punteado morado) es la única pieza fuera del control de Angular — todo lo demás es código propio de la app.
El profe arrancó un proyecto nuevo, app-05, con el esqueleto base de siempre (ng new, app.html con solo <router-outlet />) y generó un primer servicio:
$ ng generate service producto
CREATE src/app/producto.spec.ts (347 bytes)
CREATE src/app/producto.ts (121 bytes)Luego lo renombró/reescribió a mano como productoservice.ts, con un método de prueba:
ts
// productoservice.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class Productoservice {
obtenerNombreDeEjemplo(): string {
return 'laptop';
}
}@Injectable({ providedIn: 'root' })— mismo patrón ya visto en02-Conceptos.md(sección "Servicios, Inyección de Dependencias yHttpClient", con el servicioPlatillo): registra la clase como servicio, con una única instancia compartida en toda la app.obtenerNombreDeEjemplo()— un método de prueba que por ahora solo devuelve un texto fijo ('laptop') — pinta a un primer paso antes de conectar el servicio a un componente o a una API real (HttpClient).
📝 Nota de nomenclatura: el archivo se llama
productoservice.tsy la claseProductoservice(todo junto, sin mayúscula en la "S" de "Service"). La convención de Angular seríaProductoService(PascalCase, conSmayúscula) si se quiere mantener la palabra "Service" en el nombre — o simplementeProducto(sin sufijo), que es lo que generang generate service productopor defecto en versiones recientes del CLI. No es un error que rompa la compilación (TypeScript no valida el casing de los nombres), solo una inconsistencia de estilo frente a la convención habitual.
🧩 Consumiendo el servicio: ListaProductos
El profe generó un componente para usar el servicio, con un nombre poco común — carpeta y archivos con punto en vez de guion: lista.productos/lista.productos.ts (en vez del habitual lista-productos/lista-productos.ts):
ts
// lista.productos/lista.productos.ts
import { Component, inject, OnInit } from '@angular/core';
import { Productoservice } from '../productoservice';
@Component({
selector: 'app-lista-productos',
imports: [],
templateUrl: './lista.productos.html',
styleUrl: './lista.productos.css',
})
export class ListaProductos implements OnInit {
private productoService = inject(Productoservice);
nombre = '';
ngOnInit(): void {
this.nombre = this.productoService.obtenerNombreDeEjemplo();
}
}html
<!-- app.html -->
<app-lista-productos></app-lista-productos>
<router-outlet />inject(Productoservice)— misma forma funcional de pedir el servicio que ya se vio con los guards (inject(Auth)enrouting-avanzado.md); aquí, dentro de una clase (no hace falta constructor, pero podría usarse igual).ngOnInit()— llama al servicio apenas el componente está listo, y guarda el resultado ('laptop') ennombre, que el template muestra con{{ nombre }}.
🌐 provideHttpClient() — habilitando llamadas a una API
El profe agregó provideHttpClient() a app.config.ts (en app-semana04, uno de sus proyectos anteriores) para preparar el terreno hacia consumir una API real:
ts
// app.config.ts
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes, withComponentInputBinding()),
provideHttpClient(), // 👈 para habilitar la llamada al api
],
};provideHttpClient()— mismo patrón ya documentado en02-Conceptos.md(sección "Servicios, Inyección de Dependencias yHttpClient", con el ejemplo dePlatillo): activa el servicioHttpClientpara toda la app. Sin esto, cualquier servicio que inyecteHttpClientfalla en tiempo de ejecución con "no provider for HttpClient".- Se agregó también en el proyecto de práctica (
02-Ejercicios/app-05/src/app/app.config.ts), dejandoProductoservicelisto para el siguiente paso: reemplazarobtenerNombreDeEjemplo()(que hoy devuelve un texto fijo) por una llamada real conHttpClienta una API.
📝 En la captura, el comentario
//Es el mapa donde va irquedó puesto sobreprovideBrowserGlobalErrorListeners(), pero por el contenido ("el mapa") parece que estaba pensado para la línea deprovideRouter(routes, ...)(el "mapa de rutas" ya visto enrouting.md) — un comentario que se corrió de lugar, no afecta la compilación.
🔗 Conectando Productoservice a una API real
El profe pidió consumir una API pública de práctica, con productos de ejemplo. Estos son los pasos exactos, en orden, para pasar de "servicio con un dato inventado" a "servicio conectado a una API real y funcionando en pantalla":
🗺️ Diagrama: el flujo completo, de la API al pantallazo final

- API externa (morado, borde punteado) — la única pieza fuera del control de Angular.
Observable"frío" — existe pero no hace nada hasta que algo se suscribe; por eso la flecha haciasubscribe()está punteada ("sin.subscribe()no pasa nada").signal.set(datos)— el paso que existe específicamente porque el proyecto es zoneless (ver el bug real más abajo); en un proyecto con Zone.js, una propiedad normal también hubiera funcionado.- CSS encapsulado — se aplica después de que el template ya iteró los productos, no antes: primero hay que tener los
<li>en el DOM para que las clases.filaapliquen.
🪜 Paso a paso
Verificar la API por fuera de Angular primero — antes de tocar código, confirmar qué devuelve realmente el endpoint, con
curl(sin depender de Angular para saber si el problema es del servidor o del cliente):Confirma la **forma exacta** del JSON (`codigo`, `descripcion`, `precio`) — esa forma es la que define la interfaz `IProducto` del paso siguiente.$ curl https://ejerciciostutorialesya.com/vue/datos.php [{"codigo":1,"descripcion":"papas","precio":12.33}, {"codigo":2,"descripcion":"manzanas","precio":54}, {"codigo":3,"descripcion":"sandia","precio":31}]Declarar la interfaz (
IProducto) que describe esa forma, y reemplazar el método de ejemplo (obtenerNombreDeEjemplo(), que devolvía un texto fijo) por un método real que llama aHttpClient.get<IProducto[]>(url)— verproductoservice.tsmás abajo. Ya se teníaprovideHttpClient()enapp.config.tsde un paso anterior, así que no hizo falta tocar esa parte.Consumir el servicio desde el componente:
inject(Productoservice)+.obtenerProductos().subscribe(...)dentro dengOnInit(), guardando el resultado en una propiedad y mostrándola en el template con@for.Probar en el navegador — al principio la página mostraba el título "Productos" pero la lista salía vacía, sin ningún error en consola.
Diagnosticar sin asumir que era un bug de código: revisar primero la pestaña Network del navegador → la petición mostraba
200 OKcon el JSON correcto (los 3 productos) → esto descarta CORS y errores del backend/API de una. El dato SÍ llegaba.Como el dato llegaba pero la vista no cambiaba, sospechar de la detección de cambios, no del dato — se revisó si el proyecto tenía Zone.js (
grep zone.js package.json→ sin resultados) → confirmó queapp-05es un proyecto zoneless (default del CLI reciente).Corregir con la solución conocida para zoneless: convertir la propiedad normal en un signal (
productos = signal<IProducto[]>([])) y usar.set(datos)en vez dethis.productos = datos; en el template, leerlo como función (productos()).Volver a probar (esta vez con un navegador automatizado, para no depender de que la captura de pantalla coincidiera con el momento exacto de la respuesta) — confirmado: la lista ya se pinta con los 3 productos y sus precios.
💡 El orden importa: API con
curl→ código → probar en el navegador → si algo falla, revisar Network antes que el código → recién ahí sospechar de la reactividad. Saltarse pasos (por ejemplo, asumir que es un bug de Angular sin antes confirmar que el dato llegó bien) hace perder tiempo revisando el lugar equivocado.
Productoservice quedó así (reemplazando el método de ejemplo fijo):
ts
// productoservice.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface IProducto {
codigo: number;
descripcion: string;
precio: number;
}
@Injectable({
providedIn: 'root',
})
export class Productoservice {
private http = inject(HttpClient);
private readonly apiUrl = 'https://ejerciciostutorialesya.com/vue/datos.php';
obtenerProductos(): Observable<IProducto[]> {
return this.http.get<IProducto[]>(this.apiUrl);
}
}ts
// lista.productos.ts
import { Component, inject, OnInit, signal } from '@angular/core';
import { Productoservice, IProducto } from '../productoservice';
@Component({
selector: 'app-lista-productos',
imports: [],
templateUrl: './lista.productos.html',
styleUrl: './lista.productos.css',
})
export class ListaProductos implements OnInit {
private productoService = inject(Productoservice);
productos = signal<IProducto[]>([]);
ngOnInit(): void {
this.productoService.obtenerProductos().subscribe((datos) => {
this.productos.set(datos);
});
}
}html
<!-- lista.productos.html -->
<h2>Productos</h2>
<ul>
@for (producto of productos(); track producto.codigo) {
<li>{{ producto.descripcion }} — S/ {{ producto.precio }}</li>
}
</ul>IProducto— interfaz que describe la forma de cada producto tal cual la devuelve la API (codigo,descripcion,precio), mismo patrón ya usado conPlatilloen02-Conceptos.md.http.get<IProducto[]>(this.apiUrl)— pide el array de productos, tipado; retorna unObservable<IProducto[]>que no hace nada hasta que alguien se suscribe.
🐛 Error real: el HTTP respondía 200, pero la lista salía vacía
Al probar en el navegador, la petición se veía correcta (200 OK, con los 3 productos — confirmado con curl y con la pestaña Network), pero la página se quedaba sin mostrar nada, sin ningún error en consola.
🗺️ Diagrama: el camino del diagnóstico
El orden en que se descartó cada sospechoso, sin adivinar:

- Primero la consola, luego Network — sin ningún error visible, el siguiente paso es confirmar si el dato llegó, no adivinar en el código.
- El dato llegaba (200 OK) → el problema quedaba acotado a la vista, no al HTTP.
- Recién ahí se revisó si el proyecto era zoneless — la causa real del bug.
Causa: app-05 es un proyecto zoneless (Angular 22, sin zone.js en package.json — el nuevo default del CLI). Sin Zone.js, Angular no vigila automáticamente las operaciones asíncronas (HTTP incluido); una asignación normal como this.productos = datos dentro del .subscribe() no dispara una nueva detección de cambios — el dato cambia en memoria, pero la vista no se entera.
Solución: usar un signal en vez de una propiedad normal, e ir con .set():
ts
// ❌ no repinta en zoneless
productos: IProducto[] = [];
this.productos = datos;
// ✅ sí repinta (con o sin Zone.js)
productos = signal<IProducto[]>([]);
this.productos.set(datos);Y en el template, un signal se lee llamándolo como función: productos() en vez de productos.
🔗 Este es el mismo error, ya documentado en detalle, en
06-Errores/Angular-TypeScript/2026-07-04-zoneless-menu-no-se-actualiza.md(conapp-semana02-practicay el servicioPlatillo) — mismo diagnóstico, misma solución. Ver también02-Conceptos.md, sección "⚡ Reactividad, Zone.js y el problema de rendimiento", para la teoría completa de zoneless.
🧪 Tip de entrevista: "Mi HTTP responde 200 pero la vista no cambia, ¿por qué?" → En un proyecto zoneless, solo los signals (
.set()/.update()) notifican a Angular que debe repintar; asignar directamente una propiedad normal no dispara detección de cambios. Primero se confirma con la pestaña Network que el dato sí llegó bien (descarta CORS/backend); si el dato llega pero la vista no cambia, sospechar de la reactividad, no de la petición.
🎨 Dándole estilo a la lista (CSS encapsulado del componente)
Con los datos ya funcionando, se le agregó estilo con el CSS propio del componente (lista.productos.css) — no un CSS global:

html
<!-- lista.productos.html -->
<h2 class="titulo">Productos</h2>
<ul class="lista">
@for (producto of productos(); track producto.codigo) {
<li class="fila">
<span class="nombre">{{ producto.descripcion }}</span>
<span class="precio">S/ {{ producto.precio }}</span>
</li>
}
</ul>css
/* lista.productos.css */
.lista {
list-style: none;
margin: 0;
padding: 0;
max-width: 320px;
border: 1px solid #e5e7eb;
border-radius: 10px;
overflow: hidden;
}
.fila {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #e5e7eb;
}
.fila:last-child { border-bottom: none; }
.precio {
font-weight: 600;
color: #16a34a;
}styleUrl: './lista.productos.css'(ya declarado en el@Componentdesde que se generó) — Angular aplica ese CSS solo a este componente (View Encapsulation): las clases.lista,.fila,.preciono chocan con clases del mismo nombre en otro componente de la app, aunque no se les haya puesto un prefijo único.<ul>sin bullets (list-style: none) + cada<li>como una fila condisplay: flexyjustify-content: space-between— el nombre queda a la izquierda y el precio a la derecha, en vez de uno después del otro.- Se separaron
descripcionyprecioen dos<span>(.nombre/.precio) para poder darle estilo a cada uno por separado (el precio en verde y en negrita).
🧪 Tip de entrevista: ¿Por qué el CSS de un componente Angular no se filtra a otros componentes, si son clases normales sin ningún prefijo? → View Encapsulation: por defecto (
ViewEncapsulation.Emulated), Angular le agrega un atributo único a cada elemento del template y reescribe el CSS del componente para que solo aplique a elementos con ese atributo — simula el aislamiento de los Shadow DOM sin usarlo de verdad.
🏋️ Ejercicios con solución
Ejercicio 1 — Agregar un método obtenerProductoPorCodigo
Agrega un método a Productoservice que pida un solo producto por su codigo (mismo patrón que obtenerProductos(), pero con la URL parametrizada).
Ver solución
ts
// productoservice.ts
obtenerProductoPorCodigo(codigo: number): Observable<IProducto> {
return this.http.get<IProducto>(`${this.apiUrl}?codigo=${codigo}`);
}Ejercicio 2 — Mostrar un mensaje mientras carga
productos arranca vacío (signal<IProducto[]>([])) y tarda un momento en llenarse. Agrega un signal cargando que sea true hasta que llegue la respuesta, y muéstralo en el template.
Ver solución
ts
// lista.productos.ts
cargando = signal(true);
productos = signal<IProducto[]>([]);
ngOnInit(): void {
this.productoService.obtenerProductos().subscribe((datos) => {
this.productos.set(datos);
this.cargando.set(false);
});
}html
<!-- lista.productos.html -->
@if (cargando()) {
<p>Cargando productos...</p>
} @else {
<ul>
@for (producto of productos(); track producto.codigo) {
<li>{{ producto.descripcion }} — S/ {{ producto.precio }}</li>
}
</ul>
}❓ Preguntas y respuestas
1. ¿Qué significa providedIn: 'root'?
Que Angular registra el servicio como singleton global — una sola instancia compartida por toda la app, sin tener que agregarlo a ningún
providers:manualmente.
2. ¿Qué hace provideHttpClient() y qué pasa si falta?
Activa el servicio
HttpClientpara toda la app. Sin esta línea enapp.config.ts, cualquier servicio que inyecteHttpClientfalla en tiempo de ejecución con "no provider for HttpClient".
3. El HTTP responde 200 con el dato correcto, pero la vista no cambia. ¿Por qué?
En un proyecto zoneless, solo los signals (
.set()/.update()) notifican a Angular que debe repintar; asignar directamente una propiedad normal (this.productos = datos) no dispara detección de cambios.
4. ¿Por qué revisar la pestaña Network antes de sospechar del código?
Porque descarta de una si el problema es del backend/CORS o si el dato sí llegó bien — evita perder tiempo revisando el lugar equivocado (el código) cuando el HTTP en realidad ya funciona.
❓ Pendiente
- [x] Ver para qué se usa el servicio — hecho:
ListaProductoslo inyecta coninject()y muestra la lista de productos en el template. - [x]
provideHttpClient()agregado y conectado a una API real (ejerciciostutorialesya.com/vue/datos.php) — verificado corriendo, con el bug zoneless encontrado y corregido (signal en vez de propiedad normal).
📎 Apuntes relacionados
02-Conceptos.md(sección "Servicios, Inyección de Dependencias yHttpClient") — el mismo patrón@Injectable({ providedIn: 'root' })ya explicado en detalle con el servicioPlatillo.- formularios.md — Clase 10, tema anterior del mismo día.
➡️ Siguiente
Esperando más capturas de la clase para confirmar el tema y completar esta nota.