Ir al contenido

Tutorial: Rutas y menú de navegación con el Router de Angular

Al terminar este tutorial vas a ser capaz de:

  • Declarar las rutas de tu aplicación en un archivo app.routes.ts y activarlas con provideRouter.
  • Reemplazar los componentes apilados en app.html por un menú y un <router-outlet />.
  • Construir un menú con routerLink y marcar la vista activa con routerLinkActive.
  • Explicar por qué un enlace del menú necesita exact y qué pasa si se le olvida.
  • Navegar desde el código con Router cuando termina una acción, como crear un artista.
  • Comprobar en el navegador que la URL cambia sin recargar la página.
  • Dejar en verde la prueba de app.spec.ts, que el Router rompe.

Prerrequisitos: el front como quedó al final de los tutoriales del Release 4 (Compartir datos entre componentes con un servicio y El detalle de un artista), con ArtistCreate, ArtistList y ArtistaService. También conviene haber leído la teoría de Enrutamiento y Angular Router, que este tutorial aplica sin repetirla.


Hoy tu app.html apila los componentes uno debajo del otro:

app.html
<app-artist-create />
<app-artist-list />
<app-premio-list />

Funciona, pero tiene los problemas que la teoría describe: la aplicación tiene una sola URL, no se puede enviar un enlace a una vista, el botón atrás no lleva a ninguna parte y todo se pinta siempre. En este tutorial cada componente pasa a ser una vista con su propia URL:

URL Vista
/artistas ArtistList (la lista y el detalle que ya tienes)
/artistas/nuevo ArtistCreate
/premios PremioList

Si tu módulo tiene otras entidades, agrega una fila por cada una: el patrón es el mismo.


Crea src/app/app.routes.ts:

app.routes.ts
import { Routes } from '@angular/router';
import { ArtistCreate } from './artist-create/artist-create';
import { ArtistList } from './artist-list/artist-list';
import { PremioList } from './premio-list/premio-list';
export const routes: Routes = [
{ path: '', redirectTo: 'artistas', pathMatch: 'full' },
{ path: 'artistas', component: ArtistList, title: 'Artistas' },
{ path: 'artistas/nuevo', component: ArtistCreate, title: 'Nuevo artista' },
{ path: 'premios', component: PremioList, title: 'Premios' },
{ path: '**', redirectTo: 'artistas' },
];

Cada objeto asocia una URL con un componente. Fíjate en las piezas:

  • path es la parte de la URL, sin la barra inicial: 'artistas' es /artistas.
  • title cambia el título de la pestaña del navegador cuando la ruta está activa. Es lo que verás en el historial y en los marcadores.
  • { path: '', redirectTo: 'artistas', pathMatch: 'full' } manda la raíz (/) a /artistas. pathMatch: 'full' es necesario: sin él, la regla '' coincidiría con el principio de cualquier URL.
  • { path: '**', ... } es el comodín: atrapa cualquier URL que ninguna otra ruta reconoció y la manda a la lista. Va al final, porque el Router prueba las rutas en el orden en que están escritas y se queda con la primera que coincide.

Las rutas están explicadas con más detalle en Definir las rutas.


El archivo de rutas todavía no lo usa nadie. Se activa en app.config.ts, con provideRouter:

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

provideRouter(routes) va junto a provideHttpClient(): los dos registran un servicio para toda la aplicación. Ya no hay nada más que cambiar en main.ts.


Reemplaza el contenido de app.html:

app.html
<nav class="navbar navbar-expand navbar-dark bg-dark px-3">
<a class="navbar-brand" routerLink="/artistas">Música</a>
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" routerLink="/artistas">Artistas</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="/artistas/nuevo">Nuevo artista</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="/premios">Premios</a>
</li>
</ul>
</nav>
<main>
<router-outlet />
</main>

Y declara en el componente raíz las tres piezas que usa esa plantilla:

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

Las piezas son dos:

  • <router-outlet /> es el hueco donde el Router pinta el componente de la ruta activa. El menú queda fuera del hueco, así que no se vuelve a crear al navegar: lo único que cambia es lo que hay dentro de <main>.
  • routerLink reemplaza al href. Un <a href="/premios"> le pide la página al servidor y recarga toda la aplicación; routerLink solo le dice al Router que cambie de vista.

Los componentes que ya no aparecen en app.html (ArtistCreate, ArtistList, PremioList) dejan de estar en el imports del componente raíz: los importa el Router por su cuenta, a través de app.routes.ts. Quita sus import de app.ts si te quedaron.

Arranca el front (ng serve) con tu back andando y prueba: la raíz debería llevarte a /artistas, y cada enlace del menú cambia la vista y la URL.


El menú funciona, pero el usuario no sabe en qué vista está. routerLinkActive agrega una clase CSS al enlace cuya ruta está activa. Bootstrap ya trae el estilo de la clase active para nav-link:

app.html
<li class="nav-item">
<a
class="nav-link"
routerLink="/artistas"
routerLinkActive="active"
[routerLinkActiveOptions]="{ exact: true }"
ariaCurrentWhenActive="page"
>Artistas</a>
</li>
<li class="nav-item">
<a
class="nav-link"
routerLink="/artistas/nuevo"
routerLinkActive="active"
ariaCurrentWhenActive="page"
>Nuevo artista</a>
</li>
<li class="nav-item">
<a
class="nav-link"
routerLink="/premios"
routerLinkActive="active"
ariaCurrentWhenActive="page"
>Premios</a>
</li>
  • routerLinkActive="active" pone la clase active mientras la ruta del enlace esté activa.
  • ariaCurrentWhenActive="page" agrega aria-current="page" al enlace activo, que es lo que lee un lector de pantalla para decir “página actual”.
  • [routerLinkActiveOptions]="{ exact: true }" solo va en Artistas, y es la pieza que suele olvidarse.

Por qué hace falta exact: por defecto, un enlace se considera activo cuando su ruta es el principio de la URL actual. /artistas es el principio de /artistas/nuevo, así que, sin exact, estando en Nuevo artista quedan marcados los dos enlaces. Se comprobó en el navegador:

Estás en Sin exact Con exact en Artistas
/artistas/nuevo Artistas y Nuevo artista activos solo Nuevo artista activo

Con exact: true el enlace solo está activo si la URL coincide por completo. Los otros dos no lo necesitan: ningún otro enlace es el principio de su URL.


Hasta ahora el usuario cambia de vista con los enlaces. A veces es el programa el que debe llevarlo: al terminar de crear un artista, lo natural es volver a la lista. Eso se hace con el servicio Router.

En artist-create.ts, inyecta Router y llámalo cuando la creación termina bien:

artist-create.ts
import { Router } from '@angular/router';
// dentro de la clase ArtistCreate, junto a los otros servicios inyectados
private router: Router = inject(Router);
artist-create.ts
this.artistaService.crear(nuevoArtista).subscribe({
next: (artistaCreado: Artista): void => {
this.guardando.set(false);
this.exito.set(true);
this.form.reset();
this.artistaService.artistasResource.reload();
// La acción terminó: el usuario vuelve a la lista, donde ya ve el artista nuevo.
this.router.navigate(['/artistas']);
},
error: (): void => {
this.guardando.set(false);
this.error.set(true);
},
});

navigate(['/artistas']) recibe un arreglo con los segmentos de la URL. La ruta empieza con /: es absoluta, sin importar desde qué vista se llame. Hay más formas de navegar (parámetros, parámetros de consulta, navigateByUrl), explicadas en Navegar desde el código.

Fíjate en qué pasa con el aviso “Artista creado.”: como ahora se navega, la vista de creación se destruye y el mensaje no llega a verse. Si quieres mostrar una confirmación, ese mensaje debe vivir en un lugar que sobreviva a la navegación, como el servicio. Para este tutorial basta con que el artista aparezca en la lista.


Con el back y el front andando, recorre esta lista. Cada punto es una cosa que el Router resolvió por ti:

  1. Abre http://localhost:4200/. La URL cambia a /artistas y la pestaña dice “Artistas”.
  2. Haz clic en “Nuevo artista”. La URL es /artistas/nuevo, el título de la pestaña cambia y solo ese enlace está resaltado. La página no se recargó: los servicios siguen vivos, y la lista de artistas que ya habías cargado no se vuelve a pedir por esto.
  3. Pulsa el botón atrás del navegador. Vuelves a /artistas, y el resaltado también vuelve.
  4. Entra a “Premios” y recarga la página (F5). Sigues en /premios, con la vista de premios: la URL describe la vista y se puede guardar o compartir.
  5. Escribe en la barra una URL que no existe, como /no-existe. Te lleva a /artistas: es el comodín.
  6. Crea un artista. Al guardar vuelves a /artistas y el artista nuevo aparece en la lista.

9. La prueba que viene con Angular deja de funcionar

Sección titulada «9. La prueba que viene con Angular deja de funcionar»

Al crear el proyecto, Angular generó un app.spec.ts con dos pruebas del componente raíz.

Esas pruebas las ejecuta Vitest, el ejecutor de pruebas que usa Angular. Busca los archivos *.spec.ts, corre cada prueba y dice cuáles pasan y cuáles fallan. Con él se prueban los servicios, los componentes (montados en un navegador simulado, con su plantilla) y las rutas, y se pueden simular las respuestas del back para no tener que levantarlo.

La lógica es siempre la misma: preparar, actuar y comprobar. describe agrupa las pruebas de una pieza, y cada it es una prueba con un nombre que dice qué se espera. Dentro, TestBed arma el componente con las dependencias que necesita (más abajo, con un Router), y expect(valor).toBe(esperado) compara lo que pasó con lo que debía pasar: si no coinciden, la prueba falla. Todo está escrito en TypeScript. La teoría de Vitest explica cada pieza con más detalle, y el tutorial de pruebas del front con Vitest escribe las pruebas del Release 8.

Con el Router, esas pruebas se rompen, y como el pipeline del grupo corre pnpm test, tu primer PR del Release 8 quedaría en rojo. Corre ng test y verás dos fallos:

  • NG0201: No provider found for ActivatedRoute: el componente raíz ahora usa routerLink, que necesita un Router, y la prueba no lo provee.
  • La segunda prueba busca app-album-list en la raíz, y ese componente ya no está en app.html: ahora vive detrás de una ruta.

Se arreglan dándole un Router a la prueba y cambiando lo que comprueba:

app.spec.ts
import { TestBed } from '@angular/core/testing';
import { provideRouter } from '@angular/router';
import { App } from './app';
describe('App', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [App],
// El componente raíz usa routerLink y router-outlet: necesita un Router. Sin rutas
// basta, porque esta prueba solo mira el menú.
providers: [provideRouter([])],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(App);
expect(fixture.componentInstance).toBeTruthy();
});
it('should render the navigation menu', async () => {
const fixture = TestBed.createComponent(App);
await fixture.whenStable();
const enlaces: string[] = Array.from(
(fixture.nativeElement as HTMLElement).querySelectorAll('nav a.nav-link'),
).map((a: Element): string => a.getAttribute('routerLink') ?? '');
expect(enlaces).toEqual(['/artistas', '/artistas/nuevo', '/premios']);
});
});

La segunda prueba comprueba que el menú tiene los tres enlaces. Se verificó que falla si se cambia el destino de uno de ellos. Las pruebas del Router y de las vistas se escriben en el tutorial de pruebas del front con Vitest: aquí solo se deja en verde lo que ya existía.


  • El menú recarga la página. Usaste href en vez de routerLink. Se nota porque el navegador muestra el indicador de carga y la aplicación pierde su estado.
  • 'router-outlet' is not a known element. Falta importar RouterOutlet en el componente raíz.
  • Un enlace del menú que no hace nada, sin ningún error. Falta importar RouterLink: el <a> queda sin href y Angular no avisa.
  • Dos enlaces resaltados a la vez. Falta exact: true en el enlace de la ruta que es el principio de otra (sección 6).
  • La ruta comodín se come todo. Quedó antes de las demás rutas. El Router se queda con la primera que coincide: '**' siempre va al final.
  • La raíz no redirige. Falta pathMatch: 'full' en { path: '' }.
  • El componente aparece dos veces. Quedó en app.html además de en una ruta.
  • ng test falla con No provider found for ActivatedRoute. La prueba del componente raíz necesita provideRouter (sección 9).

La IA sirve bien para el esqueleto: el archivo app.routes.ts, el provideRouter y la estructura del menú. Dale la lista de tus vistas y las URL que quieres.

Lo que debes revisar tú, porque es donde suele equivocarse:

  • Que las rutas más específicas estén antes que las generales y que el comodín esté al final.
  • Que cada enlace del menú que sea el principio de otra ruta lleve exact.
  • Que no importe módulos antiguos (RouterModule, AppRoutingModule) que este curso no usa: la configuración es con provideRouter y componentes independientes.

Al final de este tutorial tienes:

  • Un app.routes.ts con una ruta por vista, una redirección de la raíz y un comodín.
  • provideRouter(routes) en app.config.ts.
  • Un menú con routerLink y routerLinkActive, y un <router-outlet /> donde se pinta cada vista.
  • Una navegación desde el código que te devuelve a la lista al crear un artista.
  • Un app.spec.ts que vuelve a pasar con el Router.

Lo que viene después, siguiendo el orden de Release 8:

  • El detalle de un recurso por ruta, donde la URL lleva el id (/artistas/:id).
  • Los componentes que asocian entidades.