Skip to content

🧰 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 con inject().
  • Cómo activar HttpClient para toda la app con provideHttpClient().
  • 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

Diagrama de arquitectura del servicio Productoservice en Angular: el usuario navega y dispara ngOnInit en ListaProductos, que inyecta Productoservice, que a su vez inyecta HttpClient (activado con provideHttpClient), que hace un HTTP GET a la API externa y devuelve un Observable que el componente consume con subscribe

  • ListaProductos no llama a HttpClient directo — pide el dato a Productoservice, que es quien conoce la URL y arma el Observable.
  • HttpClient solo existe porque provideHttpClient() lo activó en app.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:

zsh · app-05
$ 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 en 02-Conceptos.md (sección "Servicios, Inyección de Dependencias y HttpClient", con el servicio Platillo): 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.ts y la clase Productoservice (todo junto, sin mayúscula en la "S" de "Service"). La convención de Angular sería ProductoService (PascalCase, con S mayúscula) si se quiere mantener la palabra "Service" en el nombre — o simplemente Producto (sin sufijo), que es lo que genera ng generate service producto por 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) en routing-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') en nombre, 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 en 02-Conceptos.md (sección "Servicios, Inyección de Dependencias y HttpClient", con el ejemplo de Platillo): activa el servicio HttpClient para toda la app. Sin esto, cualquier servicio que inyecte HttpClient falla 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), dejando Productoservice listo para el siguiente paso: reemplazar obtenerNombreDeEjemplo() (que hoy devuelve un texto fijo) por una llamada real con HttpClient a una API.

📝 En la captura, el comentario //Es el mapa donde va ir quedó puesto sobre provideBrowserGlobalErrorListeners(), pero por el contenido ("el mapa") parece que estaba pensado para la línea de provideRouter(routes, ...) (el "mapa de rutas" ya visto en routing.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

Diagrama de flujo mostrando cómo Productoservice consume la API externa ejerciciostutorialesya.com/vue/datos.php con HttpClient.get(), retorna un Observable que no dispara nada hasta que ListaProductos.ngOnInit() se suscribe, los datos se guardan con productos.set() en un signal (necesario por ser un proyecto zoneless), el template @for itera y clasifica cada producto, se le aplican las clases CSS encapsuladas del componente, y el resultado final se pinta en pantalla con la lista de productos y sus precios

  • 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 hacia subscribe() 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 .fila apliquen.

🪜 Paso a paso

  1. 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):

    zsh · api
    $ 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}]
    Confirma la **forma exacta** del JSON (`codigo`, `descripcion`, `precio`) — esa forma es la que define la interfaz `IProducto` del paso siguiente.
  2. 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 a HttpClient.get<IProducto[]>(url) — ver productoservice.ts más abajo. Ya se tenía provideHttpClient() en app.config.ts de un paso anterior, así que no hizo falta tocar esa parte.

  3. Consumir el servicio desde el componente: inject(Productoservice) + .obtenerProductos().subscribe(...) dentro de ngOnInit(), guardando el resultado en una propiedad y mostrándola en el template con @for.

  4. 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.

  5. Diagnosticar sin asumir que era un bug de código: revisar primero la pestaña Network del navegador → la petición mostraba 200 OK con el JSON correcto (los 3 productos) → esto descarta CORS y errores del backend/API de una. El dato SÍ llegaba.

  6. 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ó que app-05 es un proyecto zoneless (default del CLI reciente).

  7. Corregir con la solución conocida para zoneless: convertir la propiedad normal en un signal (productos = signal<IProducto[]>([])) y usar .set(datos) en vez de this.productos = datos; en el template, leerlo como función (productos()).

  8. 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 con Platillo en 02-Conceptos.md.
  • http.get<IProducto[]>(this.apiUrl) — pide el array de productos, tipado; retorna un Observable<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:

Diagrama de diagnóstico de un bug de Angular zoneless: la página muestra Productos pero la lista sale vacía, se revisa primero si hay error en consola, luego la pestaña Network para confirmar que el HTTP responde 200 con el JSON correcto, y finalmente si el proyecto tiene zone.js en package.json, concluyendo que al ser zoneless hay que cambiar la propiedad normal por un signal con .set(datos)

  • 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 (con app-semana02-practica y el servicio Platillo) — mismo diagnóstico, misma solución. Ver también 02-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 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:

Resultado final en el navegador: tarjeta "Productos" con Papas S/12.33, Manzanas S/54 y Sandia S/31, sin bullets, nombre a la izquierda y precio en verde y negrita a la derecha

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 @Component desde que se generó) — Angular aplica ese CSS solo a este componente (View Encapsulation): las clases .lista, .fila, .precio no 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 con display: flex y justify-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 descripcion y precio en 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 HttpClient para toda la app. Sin esta línea en app.config.ts, cualquier servicio que inyecte HttpClient falla 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: ListaProductos lo inyecta con inject() 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 y HttpClient") — el mismo patrón @Injectable({ providedIn: 'root' }) ya explicado en detalle con el servicio Platillo.
  • 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.