Saltar al contenido principal
Logo FPCode

Angular avanzado: HTTP, rutas, guards y datos

Contenido

Tutorial complementario avanzado de Angular para Desarrollo web en entorno cliente: servicios HTTP, rutas, parámetros, guards, interceptores, autenticación conceptual, formularios reactivos, manejo de errores y despliegue.

Introducción

En el tutorial anterior viste los fundamentos de Angular: componentes standalone, plantillas, binding, señales, formularios simples, servicios e inyección de dependencias. Este tutorial complementario avanza hacia aplicaciones cliente más completas: varias pantallas, datos remotos, rutas con parámetros, guards, interceptores HTTP, formularios reactivos y consideraciones básicas de seguridad.

El objetivo es comprender cómo se estructura una aplicación Angular que consume una API. Angular no se conecta directamente a una base de datos desde el navegador. El cliente Angular se comunica con un backend mediante HTTP, normalmente usando JSON. El backend es quien valida, autentica, autoriza y accede a la base de datos.

Trabajaremos con patrones actuales de Angular: configuración mediante providers, provideRouter, provideHttpClient, servicios inyectables, guards funcionales, interceptores funcionales y componentes standalone.

Conocimiento previo

Referencias

Índice

  1. Arquitectura cliente-servidor con Angular
  2. Configurar rutas con provideRouter
  3. Navegación entre pantallas
  4. Rutas con parámetros
  5. Configurar HttpClient con provideHttpClient
  6. Servicios para consumir APIs
  7. Cargar datos en un componente
  8. Manejo de errores HTTP
  9. Crear, actualizar y eliminar datos
  10. Formularios reactivos
  11. Guards para proteger rutas
  12. Interceptores HTTP
  13. Autenticación con tokens: enfoque conceptual
  14. Organización por funcionalidades
  15. Proyecto guiado: panel de productos con API 15.1. Definir rutas de la aplicación 15.2. Crear el modelo Producto 15.3. Crear el servicio HTTP de productos 15.4. Crear la página de listado 15.5. Crear la página de detalle 15.6. Crear un formulario reactivo de edición 15.7. Añadir guard e interceptor
  16. Build y despliegue
  17. Buenas prácticas de Angular avanzado
  18. Ejercicios

1. Arquitectura cliente-servidor con Angular

Una aplicación Angular se ejecuta en el navegador. Su trabajo principal es mostrar interfaz, recoger acciones del usuario, validar de forma inicial y comunicarse con servicios remotos. La lógica de negocio crítica, la autenticación real y el acceso a base de datos deben estar en el servidor.

Arquitectura típica:

Navegador
Angular
Componentes
Servicios
Rutas
Formularios
|
| HTTP JSON
v
Servidor API
Validación
Autenticación
Reglas de negocio
Acceso a base de datos
|
v
Base de datos

Angular no debe conectarse directamente a MySQL, PostgreSQL, MongoDB u otra base de datos desde el cliente. Si encuentras ejemplos antiguos que mezclan Angular y base de datos directamente desde el navegador, descártalos como arquitectura insegura o incorrecta para una aplicación real.

2. Configurar rutas con provideRouter

Las rutas permiten asociar URLs a componentes. En Angular actual se configuran normalmente con provideRouter.

Archivo app.routes.ts:

import { Routes } from '@angular/router';
import { ProductosPage } from './features/productos/productos-page';
import { ProductoDetallePage } from './features/productos/producto-detalle-page';
import { LoginPage } from './features/auth/login-page';
export const routes: Routes = [
{ path: '', redirectTo: 'productos', pathMatch: 'full' },
{ path: 'productos', component: ProductosPage },
{ path: 'productos/:id', component: ProductoDetallePage },
{ path: 'login', component: LoginPage },
{ path: '**', redirectTo: 'productos' },
];

Archivo app.config.ts:

import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes)],
};

El componente principal debe incluir el lugar donde se renderiza la pantalla activa:

<router-outlet></router-outlet>

Para usarlo en un componente standalone, importa RouterOutlet:

import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
imports: [RouterOutlet],
templateUrl: './app.html',
})
export class App {}

3. Navegación entre pantallas

Para navegar sin recargar la página se usa routerLink.

import { Component } from '@angular/core';
import { RouterLink, RouterLinkActive } from '@angular/router';
@Component({
selector: 'app-nav',
imports: [RouterLink, RouterLinkActive],
templateUrl: './nav.html',
})
export class Nav {}

Plantilla:

<nav>
<a routerLink="/productos" routerLinkActive="activo">Productos</a>
<a routerLink="/login" routerLinkActive="activo">Login</a>
</nav>

También puedes navegar desde TypeScript inyectando Router:

import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
@Component({
selector: 'app-login-page',
templateUrl: './login-page.html',
})
export class LoginPage {
private router = inject(Router);
entrar() {
this.router.navigate(['/productos']);
}
}

Usa navegación programática cuando la ruta dependa de una acción lógica, como un login correcto o la creación de un recurso.

4. Rutas con parámetros

Una ruta con parámetro permite representar recursos concretos:

{ path: 'productos/:id', component: ProductoDetallePage }

Para leer el parámetro puedes inyectar ActivatedRoute:

import { Component, inject, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-producto-detalle-page',
templateUrl: './producto-detalle-page.html',
})
export class ProductoDetallePage {
private route = inject(ActivatedRoute);
productoId = signal<number | null>(null);
ngOnInit() {
const id = this.route.snapshot.paramMap.get('id');
this.productoId.set(id ? Number(id) : null);
}
}

Plantilla:

@if (productoId() !== null) {
<p>Detalle del producto {{ productoId() }}</p>
} @else {
<p>No se ha recibido un identificador válido.</p>
}

El parámetro llega como texto. Si necesitas un número, conviértelo y valida el resultado.

5. Configurar HttpClient con provideHttpClient

HttpClient es el servicio oficial de Angular para hacer peticiones HTTP. Se configura en los providers de la aplicación.

Archivo app.config.ts:

import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(),
],
};

Una vez configurado, puedes inyectar HttpClient en servicios o componentes. En una aplicación mantenible, lo habitual es usarlo desde servicios, no directamente desde todos los componentes.

6. Servicios para consumir APIs

Un servicio HTTP concentra las llamadas a la API y ofrece métodos con nombres del dominio de la aplicación.

Modelo producto.ts:

export type Producto = {
id: number;
nombre: string;
precio: number;
categoria: string;
};

Servicio productos-api.service.ts:

import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Producto } from './producto';
@Injectable({ providedIn: 'root' })
export class ProductosApiService {
private http = inject(HttpClient);
private apiUrl = '/api/productos';
obtenerProductos() {
return this.http.get<Producto[]>(this.apiUrl);
}
obtenerProducto(id: number) {
return this.http.get<Producto>(`${this.apiUrl}/${id}`);
}
}

HttpClient devuelve observables. Para esta unidad basta con entender que un observable representa un dato que llegará de forma asíncrona y al que podemos suscribirnos.

7. Cargar datos en un componente

Un componente puede llamar al servicio y guardar el resultado en señales.

import { Component, inject, signal } from '@angular/core';
import { ProductosApiService } from './productos-api.service';
import { Producto } from './producto';
@Component({
selector: 'app-productos-page',
templateUrl: './productos-page.html',
})
export class ProductosPage {
private productosApi = inject(ProductosApiService);
productos = signal<Producto[]>([]);
cargando = signal(false);
error = signal<string | null>(null);
ngOnInit() {
this.cargarProductos();
}
cargarProductos() {
this.cargando.set(true);
this.error.set(null);
this.productosApi.obtenerProductos().subscribe({
next: (productos) => {
this.productos.set(productos);
this.cargando.set(false);
},
error: () => {
this.error.set('No se pudieron cargar los productos.');
this.cargando.set(false);
},
});
}
}

Plantilla:

@if (cargando()) {
<p>Cargando productos...</p>
} @else if (error()) {
<p role="alert">{{ error() }}</p>
} @else {
<ul>
@for (producto of productos(); track producto.id) {
<li>{{ producto.nombre }} - {{ producto.precio }} €</li>
} @empty {
<li>No hay productos disponibles.</li>
}
</ul>
}

Los estados de carga, error y lista vacía forman parte de la experiencia de usuario. No son detalles opcionales.

8. Manejo de errores HTTP

Una petición puede fallar por muchas razones: servidor no disponible, URL incorrecta, falta de permisos, datos inválidos o error de red. El componente debe mostrar un mensaje claro, pero sin exponer información sensible.

Puedes centralizar parte del tratamiento en el servicio:

import { catchError, throwError } from 'rxjs';
obtenerProductos() {
return this.http.get<Producto[]>(this.apiUrl).pipe(
catchError((error) => {
console.error('Error al cargar productos', error);
return throwError(() => new Error('No se pudieron cargar los productos'));
}),
);
}

Y mostrar un mensaje desde el componente:

this.productosApi.obtenerProductos().subscribe({
next: (productos) => this.productos.set(productos),
error: (error) => this.error.set(error.message),
});

Buenas prácticas:

  • Muestra mensajes comprensibles para el usuario.
  • Registra información técnica solo donde sea apropiado.
  • No muestres tokens, trazas internas ni datos sensibles.
  • Distingue entre “no hay datos” y “ha fallado la carga”.
  • Ofrece una acción de reintento si tiene sentido.

9. Crear, actualizar y eliminar datos

Además de leer datos con GET, una aplicación puede enviar datos al servidor.

Servicio con operaciones CRUD básicas:

crearProducto(producto: Omit<Producto, 'id'>) {
return this.http.post<Producto>(this.apiUrl, producto);
}
actualizarProducto(id: number, producto: Partial<Producto>) {
return this.http.patch<Producto>(`${this.apiUrl}/${id}`, producto);
}
eliminarProducto(id: number) {
return this.http.delete<void>(`${this.apiUrl}/${id}`);
}

Uso desde un componente:

eliminar(id: number) {
this.productosApi.eliminarProducto(id).subscribe({
next: () => {
this.productos.update((productos) =>
productos.filter((producto) => producto.id !== id),
);
},
error: () => {
this.error.set('No se pudo eliminar el producto.');
},
});
}

Aunque actualices la interfaz de forma inmediata, el servidor sigue siendo la fuente de verdad. Si el servidor rechaza la operación, debes informar al usuario y mantener la interfaz coherente.

10. Formularios reactivos

Los formularios reactivos de Angular son adecuados cuando hay varios campos, validación, estados de error o edición de datos procedentes de una API.

Componente:

import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
@Component({
selector: 'app-producto-form',
imports: [ReactiveFormsModule],
templateUrl: './producto-form.html',
})
export class ProductoForm {
private formBuilder = inject(FormBuilder);
productoForm = this.formBuilder.nonNullable.group({
nombre: ['', [Validators.required, Validators.minLength(3)]],
precio: [0, [Validators.required, Validators.min(0.01)]],
categoria: ['hardware', [Validators.required]],
});
guardar() {
if (this.productoForm.invalid) {
this.productoForm.markAllAsTouched();
return;
}
const producto = this.productoForm.getRawValue();
console.log(producto);
}
}

Plantilla:

<form [formGroup]="productoForm" (ngSubmit)="guardar()">
<label for="nombre">Nombre</label>
<input id="nombre" formControlName="nombre" />
@if (productoForm.controls.nombre.touched && productoForm.controls.nombre.invalid) {
<p id="error-nombre" role="alert">El nombre debe tener al menos 3 caracteres.</p>
}
<label for="precio">Precio</label>
<input id="precio" type="number" formControlName="precio" />
<label for="categoria">Categoría</label>
<select id="categoria" formControlName="categoria">
<option value="hardware">Hardware</option>
<option value="software">Software</option>
</select>
<button type="submit">Guardar</button>
</form>

Los formularios reactivos separan el modelo del formulario de la plantilla y facilitan validaciones más complejas.

11. Guards para proteger rutas

Un guard decide si una ruta puede activarse. Se usa, por ejemplo, para impedir que un usuario no autenticado acceda a una zona privada.

Servicio conceptual de autenticación:

import { Injectable, signal } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class AuthService {
private token = signal<string | null>(null);
estaAutenticado() {
return this.token() !== null;
}
obtenerToken() {
return this.token();
}
}

Guard funcional:

import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.estaAutenticado()) {
return true;
}
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url },
});
};

Uso en rutas:

export const routes: Routes = [
{ path: 'productos', component: ProductosPage },
{
path: 'admin',
component: AdminPage,
canActivate: [authGuard],
},
];

12. Interceptores HTTP

Un interceptor HTTP permite procesar peticiones o respuestas de forma centralizada. Se usa para añadir cabeceras, registrar errores, adjuntar tokens o aplicar lógica común.

Interceptor funcional para añadir token:

import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from './auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthService);
const token = auth.obtenerToken();
if (!token) {
return next(req);
}
const requestConToken = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`,
},
});
return next(requestConToken);
};

Configuración:

import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './features/auth/auth.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(withInterceptors([authInterceptor])),
],
};

Los interceptores evitan repetir la misma lógica en todos los servicios HTTP.

13. Autenticación con tokens: enfoque conceptual

En muchas aplicaciones, el usuario inicia sesión y el servidor devuelve un token. Ese token se envía después en peticiones protegidas. El flujo conceptual es:

  1. El usuario introduce credenciales.
  2. Angular envía las credenciales al backend por HTTPS.
  3. El backend valida las credenciales.
  4. El backend devuelve una respuesta de autenticación.
  5. Angular conserva el estado de sesión según la estrategia definida.
  6. Las peticiones protegidas incluyen la información necesaria para autorizar la operación.

Puntos importantes de seguridad:

  • Usa siempre HTTPS en producción.
  • No guardes contraseñas en el cliente.
  • No confíes en validaciones del cliente para proteger datos.
  • Trata localStorage y sessionStorage con cautela porque son accesibles desde JavaScript.
  • Considera riesgos de XSS y CSRF según la estrategia de sesión usada.
  • El servidor debe validar permisos aunque Angular oculte botones o rutas.

Este tutorial no implementa un sistema de autenticación real completo. Su objetivo es que entiendas dónde encajan guards e interceptores dentro de la arquitectura.

14. Organización por funcionalidades

Cuando Angular crece, suele ser más mantenible organizar el código por funcionalidades.

src/app/
core/
auth.service.ts
auth.interceptor.ts
auth.guard.ts
features/
productos/
productos-page.ts
productos-page.html
producto-detalle-page.ts
producto-form.ts
productos-api.service.ts
producto.ts
auth/
login-page.ts
login-page.html
shared/
components/
estado-carga.ts
mensaje-error.ts
app.config.ts
app.routes.ts

Responsabilidades:

CarpetaContenido
coreServicios globales, autenticación, guards, interceptores.
featuresPantallas y servicios propios de cada funcionalidad.
sharedComponentes o utilidades reutilizables sin lógica de negocio específica.

No uses esta estructura como una obligación rígida. Úsala cuando el proyecto ya tenga varias funcionalidades y empiece a necesitar separación.

15. Proyecto guiado: panel de productos con API

El proyecto será un panel de productos con rutas, listado, detalle, formulario de edición, servicio HTTP, guard e interceptor conceptual.

15.1. Definir rutas de la aplicación

export const routes: Routes = [
{ path: '', redirectTo: 'productos', pathMatch: 'full' },
{ path: 'productos', component: ProductosPage },
{ path: 'productos/:id', component: ProductoDetallePage },
{
path: 'admin/productos/:id/editar',
component: ProductoFormPage,
canActivate: [authGuard],
},
{ path: 'login', component: LoginPage },
{ path: '**', redirectTo: 'productos' },
];

Esta estructura separa navegación pública y zona de administración.

15.2. Crear el modelo Producto

export type Producto = {
id: number;
nombre: string;
descripcion: string;
precio: number;
categoria: string;
stock: number;
};
export type ProductoEntrada = Omit<Producto, 'id'>;

ProductoEntrada representa los datos que se envían al crear o editar, sin id.

15.3. Crear el servicio HTTP de productos

@Injectable({ providedIn: 'root' })
export class ProductosApiService {
private http = inject(HttpClient);
private apiUrl = '/api/productos';
listar() {
return this.http.get<Producto[]>(this.apiUrl);
}
obtener(id: number) {
return this.http.get<Producto>(`${this.apiUrl}/${id}`);
}
actualizar(id: number, producto: ProductoEntrada) {
return this.http.put<Producto>(`${this.apiUrl}/${id}`, producto);
}
eliminar(id: number) {
return this.http.delete<void>(`${this.apiUrl}/${id}`);
}
}

Los componentes no deben construir URLs a mano si existe un servicio encargado de esa responsabilidad.

15.4. Crear la página de listado

@Component({
selector: 'app-productos-page',
imports: [RouterLink],
templateUrl: './productos-page.html',
})
export class ProductosPage {
private productosApi = inject(ProductosApiService);
productos = signal<Producto[]>([]);
cargando = signal(false);
error = signal<string | null>(null);
ngOnInit() {
this.cargar();
}
cargar() {
this.cargando.set(true);
this.error.set(null);
this.productosApi.listar().subscribe({
next: (productos) => {
this.productos.set(productos);
this.cargando.set(false);
},
error: () => {
this.error.set('No se pudo cargar el listado.');
this.cargando.set(false);
},
});
}
}

Plantilla:

<h1>Productos</h1>
@if (cargando()) {
<p>Cargando...</p>
} @else if (error()) {
<p role="alert">{{ error() }}</p>
} @else {
<ul>
@for (producto of productos(); track producto.id) {
<li>
<a [routerLink]="['/productos', producto.id]">
{{ producto.nombre }}
</a>
</li>
} @empty {
<li>No hay productos.</li>
}
</ul>
}

15.5. Crear la página de detalle

@Component({
selector: 'app-producto-detalle-page',
imports: [RouterLink],
templateUrl: './producto-detalle-page.html',
})
export class ProductoDetallePage {
private route = inject(ActivatedRoute);
private productosApi = inject(ProductosApiService);
producto = signal<Producto | null>(null);
error = signal<string | null>(null);
ngOnInit() {
const id = Number(this.route.snapshot.paramMap.get('id'));
if (Number.isNaN(id)) {
this.error.set('Identificador no válido.');
return;
}
this.productosApi.obtener(id).subscribe({
next: (producto) => this.producto.set(producto),
error: () => this.error.set('No se pudo cargar el producto.'),
});
}
}

Plantilla:

@if (error()) {
<p role="alert">{{ error() }}</p>
} @else if (producto()) {
<article>
<h1>{{ producto()!.nombre }}</h1>
<p>{{ producto()!.descripcion }}</p>
<p>{{ producto()!.precio }} €</p>
<a [routerLink]="['/admin/productos', producto()!.id, 'editar']">
Editar
</a>
</article>
} @else {
<p>Cargando producto...</p>
}

15.6. Crear un formulario reactivo de edición

@Component({
selector: 'app-producto-form-page',
imports: [ReactiveFormsModule],
templateUrl: './producto-form-page.html',
})
export class ProductoFormPage {
private formBuilder = inject(FormBuilder);
private route = inject(ActivatedRoute);
private router = inject(Router);
private productosApi = inject(ProductosApiService);
productoId = Number(this.route.snapshot.paramMap.get('id'));
error = signal<string | null>(null);
form = this.formBuilder.nonNullable.group({
nombre: ['', [Validators.required, Validators.minLength(3)]],
descripcion: ['', [Validators.required]],
precio: [0, [Validators.required, Validators.min(0.01)]],
categoria: ['', [Validators.required]],
stock: [0, [Validators.required, Validators.min(0)]],
});
guardar() {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}
this.productosApi.actualizar(this.productoId, this.form.getRawValue()).subscribe({
next: () => this.router.navigate(['/productos', this.productoId]),
error: () => this.error.set('No se pudo guardar el producto.'),
});
}
}

Plantilla simplificada:

<form [formGroup]="form" (ngSubmit)="guardar()">
<label for="nombre">Nombre</label>
<input id="nombre" formControlName="nombre" />
<label for="descripcion">Descripción</label>
<textarea id="descripcion" formControlName="descripcion"></textarea>
<label for="precio">Precio</label>
<input id="precio" type="number" formControlName="precio" />
<label for="categoria">Categoría</label>
<input id="categoria" formControlName="categoria" />
<label for="stock">Stock</label>
<input id="stock" type="number" formControlName="stock" />
@if (error()) {
<p role="alert">{{ error() }}</p>
}
<button type="submit">Guardar</button>
</form>

15.7. Añadir guard e interceptor

Guard para proteger edición:

export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService);
const router = inject(Router);
return auth.estaAutenticado()
? true
: router.createUrlTree(['/login']);
};

Interceptor para adjuntar token:

export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthService).obtenerToken();
return next(
token
? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })
: req,
);
};

Configuración:

export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(withInterceptors([authInterceptor])),
],
};

Con esto, la pantalla de edición queda protegida en el cliente y las peticiones pueden incluir token cuando exista.

16. Build y despliegue

Para generar una versión de producción:

Terminal window
ng build

Angular crea una salida optimizada en la carpeta de distribución configurada por el proyecto. Esa carpeta contiene archivos estáticos que pueden servirse desde un servidor web o una plataforma de despliegue.

Aspectos que debes revisar antes de desplegar:

  • La URL base de la API no debe quedar hardcodeada de forma difícil de cambiar.
  • La aplicación debe servirse por HTTPS en producción.
  • El servidor debe estar configurado para devolver la aplicación en rutas internas de la SPA si se usa routing de cliente.
  • No deben publicarse secretos, claves privadas ni credenciales en el bundle.
  • Las variables de entorno y configuración deben tratarse según la herramienta de build y el entorno de despliegue.

17. Buenas prácticas de Angular avanzado

  • Usa servicios para acceder a APIs, no peticiones duplicadas en cada componente.
  • Define tipos TypeScript para las respuestas importantes.
  • Muestra siempre carga, error y estado vacío.
  • Protege rutas con guards cuando mejore la navegación, pero valida permisos en backend.
  • Usa interceptores para lógica HTTP transversal.
  • No guardes secretos en el cliente.
  • No conectes Angular directamente a la base de datos.
  • Usa formularios reactivos cuando haya validación o edición compleja.
  • Organiza por funcionalidades cuando el proyecto crezca.
  • Revisa documentación actual antes de copiar sintaxis antigua de Angular.

18. Ejercicios

Ejercicio 1: Configurar rutas

Crea una aplicación Angular con rutas para inicio, listado de productos, detalle de producto y login.

Ejercicio 2: Navegación con routerLink

Crea una navegación principal usando routerLink y marca el enlace activo con routerLinkActive.

Ejercicio 3: Ruta con parámetro

Crea una ruta /productos/:id y muestra el identificador recibido en la página de detalle.

Ejercicio 4: Servicio HTTP

Crea un servicio ProductosApiService con métodos para listar y obtener un producto por identificador.

Ejercicio 5: Estados de carga y error

Consume una API o JSON local y muestra mensajes de carga, error, lista vacía y resultados.

Ejercicio 6: Operación de eliminación

Añade un método para eliminar productos y actualiza la lista local cuando el servidor confirme la operación.

Ejercicio 7: Formulario reactivo

Crea un formulario reactivo de producto con validación de nombre, precio, categoría y stock.

Ejercicio 8: Guard de autenticación

Crea un guard funcional que redirija a /login cuando el usuario no esté autenticado.

Ejercicio 9: Interceptor de token

Crea un interceptor funcional que añada una cabecera Authorization cuando exista token en el servicio de autenticación.

Ejercicio 10: Proyecto integrador

Construye el panel de productos del tutorial con rutas, listado, detalle, formulario reactivo, servicio HTTP, guard e interceptor.