Lección 3 de 313 min
Angular Router
Rutas, parámetros, carga diferida, guards, resolvers y navegación programática.
Tu tienda tiene página de inicio, catálogo, ficha de cada producto, carrito, área de cliente y panel de administración. Cada pantalla necesita su propia dirección, para que el usuario pueda guardarla, compartirla o volver atrás con el navegador, y todo sin recargar la página, porque es una SPA.
De eso se encarga Angular Router: asocia cada URL a un componente, lee los datos que viajan en la dirección, carga las secciones pesadas solo cuando hacen falta y decide si el usuario puede entrar o salir de cada pantalla.
Definir rutas y enlazarlas
Routes, provideRouter y router-outlet
Una lista de rutas, un proveedor que la activa y un hueco donde se pinta cada página.
Las rutas son un array de tipo Routes, normalmente en app.routes.ts: cada objeto asocia un path (sin barra inicial) a un component. Ese array se registra en app.config.ts con provideRouter(routes). El router compara la URL con las rutas en orden y usa la primera que coincide.
El componente de la ruta activa se pinta dentro de <router-outlet />, que suele estar en la plantilla del componente raíz (importando RouterOutlet), entre una cabecera y un pie que no cambian. Si das un title a cada ruta, el router también actualiza el título de la pestaña.
Úsalo cuando
En cualquier aplicación con más de una pantalla. Los proyectos creados con ng new ya traen app.routes.ts y provideRouter configurados.
Ten en cuenta
El orden importa: si productos/:id va antes que productos/nuevo, el router tomará «nuevo» como si fuera un id. Pon primero las rutas más específicas.
// app.routes.ts
export const routes: Routes = [
{ path: "", component: Inicio, title: "Inicio" },
{ path: "productos", component: ListaProductos, title: "Productos" },
{ path: "productos/:id", component: DetalleProducto },
];
<!-- app.html: lo que rodea al outlet se ve en todas las páginas -->
<app-cabecera />
<main>
<router-outlet />
</main>routerLink y routerLinkActive
Enlaces que navegan sin recargar y que saben si apuntan a la página actual.
Con un <a href> normal, el navegador pediría una página nueva al servidor. Con la directiva routerLink, el router intercepta el clic y cambia de vista sin recargar: <a routerLink="/productos">. Para rutas con partes variables, pasa un array: [routerLink]="['/productos', p.id]"; para parámetros de consulta, añade [queryParams].
routerLinkActive="activo" añade esa clase CSS al enlace mientras su ruta está activa, lo que sirve para resaltar la sección actual del menú. Las dos directivas se importan en el componente: RouterLink y RouterLinkActive.
Úsalo cuando
Para todos los enlaces internos de la aplicación. Para enlaces a otras webs, usa un <a href> normal.
Ten en cuenta
routerLinkActive también marca los enlaces a rutas «padre»: el de / quedaría activo en todas las páginas. Usa [routerLinkActiveOptions]="{ exact: true }" en ese caso, y añade ariaCurrentWhenActive="page" para que los lectores de pantalla anuncien la página actual.
<nav>
<a routerLink="/" routerLinkActive="activo"
[routerLinkActiveOptions]="{ exact: true }">Inicio</a>
<a routerLink="/productos" routerLinkActive="activo"
ariaCurrentWhenActive="page">Productos</a>
</nav>
@for (p of productos(); track p.id) {
<a [routerLink]="['/productos', p.id]" [queryParams]="{ origen: 'lista' }">
{{ p.nombre }}
</a>
}Datos en la URL
Parámetros de ruta y de consulta
Leer /productos/42 y ?orden=precio desde el componente con ActivatedRoute.
Un parámetro de ruta se declara con dos puntos en el path (productos/:id) y forma parte de la dirección: identifica qué se muestra. Un parámetro de consulta (query param) va tras el ? (/productos?orden=precio&pagina=2) y suele indicar cómo se muestra: filtros, orden, página. Los dos llegan siempre como texto.
El servicio ActivatedRoute da acceso a la ruta activa: paramMap y queryParamMap son Observables que emiten cada vez que cambian, y snapshot ofrece los valores de ese instante. Si el usuario pasa de /productos/1 a /productos/2, Angular reutiliza el mismo componente y solo cambian los parámetros, así que el snapshot leído al principio se queda viejo.
Úsalo cuando
Parámetros de ruta para identificar un recurso, como un producto o un pedido; de consulta, para el estado que el usuario quiere compartir o guardar en favoritos, como los filtros de una búsqueda.
Ten en cuenta
Convierte los valores (Number(params.get('id'))) y no te fíes de ellos: el usuario puede escribir cualquier cosa en la barra de direcciones.
export class DetalleProducto {
private ruta = inject(ActivatedRoute);
private catalogo = inject(Catalogo);
// Se actualiza al pasar de /productos/1 a /productos/2
producto = toSignal(
this.ruta.paramMap.pipe(
switchMap((params) => this.catalogo.obtener(Number(params.get("id")))),
),
);
orden = this.ruta.snapshot.queryParamMap.get("orden") ?? "relevancia";
}withComponentInputBinding
Recibe los datos de la URL directamente como inputs del componente.
Con provideRouter(routes, withComponentInputBinding()), el router asigna a los inputs del componente de la ruta los valores con el mismo nombre: parámetros de ruta, parámetros de consulta, data de la ruta y resultados de resolvers. Si la ruta es productos/:id, basta con declarar id = input<string>().
El código queda más corto y el componente no sabe nada del router, así que puedes reutilizarlo o probarlo pasándole el input a mano. Como el input es una señal, derivas valores con computed() y se recalculan solos cuando cambia la URL.
Úsalo cuando
En proyectos nuevos, casi siempre: es la forma más directa de leer la URL.
Ten en cuenta
Si un dato no está en la URL, el input queda undefined: dale un tipo que lo admita o un valor por defecto. Y evita repetir nombres: si un query param y un parámetro de ruta se llaman igual, solo uno llegará al input.
// app.config.ts
provideRouter(routes, withComponentInputBinding())
// Ruta: { path: "productos/:id", component: DetalleProducto }
// URL: /productos/42?pestana=opiniones
export class DetalleProducto {
id = input.required<string>(); // "42"
pestana = input("descripcion"); // "opiniones"
idNumerico = computed(() => Number(this.id()));
}Organizar las rutas
Rutas anidadas
Rutas dentro de rutas: una sección con su propio menú y su propio router-outlet.
Con children, una ruta contiene otras: cuenta puede tener cuenta/perfil y cuenta/pedidos. El componente padre se pinta en el outlet principal y tiene en su plantilla otro <router-outlet />, donde aparece el hijo activo. Así el menú lateral de la sección se mantiene mientras cambia el contenido.
Las rutas hijas escriben su path relativo al padre (perfil, no cuenta/perfil). Una hija con path: '' es la que se muestra cuando la URL es exactamente la del padre.
Úsalo cuando
Para secciones con estructura propia: el área de cliente, un panel de administración, un asistente de varios pasos con una URL por paso.
Ten en cuenta
Dentro de la sección, los enlaces sin barra inicial (routerLink="pedidos") son relativos a la ruta actual. Es cómodo, pero si mueves la sección de sitio, comprueba que sigan apuntando bien.
{
path: "cuenta",
component: Cuenta, // su plantilla tiene un menú y un <router-outlet />
children: [
{ path: "", component: Resumen },
{ path: "perfil", component: Perfil },
{ path: "pedidos", component: Pedidos },
],
}Redirecciones y la ruta comodín
Envía las direcciones antiguas a las nuevas y recoge las que no existen.
Una ruta con redirectTo no pinta nada: envía a otra dirección. Con pathMatch: 'full' solo redirige si la URL coincide entera; es obligatorio al redirigir desde path: '', porque una cadena vacía es el principio de cualquier URL. También sirve para que sigan funcionando direcciones antiguas.
La ruta comodín ** coincide con cualquier URL. Como el router usa la primera coincidencia, va la última de la lista y suele mostrar una página de «No encontrada».
Úsalo cuando
Para la página por defecto de una sección, para no romper enlaces antiguos tras cambiar las URLs y para tener siempre una página 404 amable.
Ten en cuenta
Una ** en medio de la lista se tragaría todas las rutas que vienen detrás. En las versiones recientes, redirectTo también admite una función, por si la redirección depende de datos.
export const routes: Routes = [
{ path: "", redirectTo: "productos", pathMatch: "full" },
{ path: "productos", component: ListaProductos },
{ path: "tienda", redirectTo: "productos" }, // dirección antigua
{ path: "**", component: NoEncontrada }, // siempre la última
];Carga diferida: loadComponent y loadChildren
Descargar el código de una sección solo cuando el usuario entra en ella.
En lugar de component, una ruta puede usar loadComponent: () => import('./informes/informes'). Ese import() dinámico hace que el bundler saque el componente, y lo que solo él usa, a un archivo aparte que se descarga la primera vez que alguien visita la ruta. La carga inicial pesa menos.
loadChildren hace lo mismo con un grupo entero de rutas: apunta a un archivo que exporta un array Routes, como admin.routes.ts. Es la forma natural de dividir una aplicación grande por secciones.
Úsalo cuando
Para secciones que no visitan todos los usuarios o que pesan mucho: el panel de administración, los informes, la configuración.
Ten en cuenta
Si el archivo exporta el componente o las rutas con export default, te ahorras el .then((m) => m.Informes). Y si importas ese mismo componente de forma normal en otro sitio, el bundler ya no puede separarlo.
export const routes: Routes = [
{
path: "informes",
loadComponent: () => import("./informes/informes").then((m) => m.Informes),
},
{
path: "admin",
loadChildren: () => import("./admin/admin.routes").then((m) => m.ADMIN_ROUTES),
},
];Proteger, preparar y dirigir la navegación
Un guard es una función que el router consulta antes de completar una navegación. Puede devolver true (adelante), false (cancelar) o un UrlTree (redirigir a otra parte), directamente o dentro de una Promise o un Observable. Como se ejecuta en un contexto de inyección, puede usar inject().
CanActivateFn
¿Puede el usuario entrar en esta ruta?
Un guard canActivate se ejecuta cuando el router ya ha encontrado la ruta y antes de mostrarla. Es el caso clásico de la zona privada: si hay sesión, devuelve true; si no, devuelve un UrlTree hacia el login, con la dirección de destino en un query param para volver después.
Úsalo cuando
Para proteger rutas que requieren sesión, un rol o haber completado un paso previo.
Ten en cuenta
Un guard mejora la experiencia, no es seguridad: el código de la ruta sigue en el navegador y cualquiera puede leerlo. La API debe comprobar los permisos en cada petición, como verás en el módulo de autenticación.
export const requiereSesion: CanActivateFn = (ruta, estado) => {
const sesion = inject(Sesion);
const router = inject(Router);
return sesion.activa()
? true
: router.createUrlTree(["/login"], { queryParams: { volver: estado.url } });
};
// app.routes.ts
{ path: "cuenta", component: Cuenta, canActivate: [requiereSesion] }CanMatchFn
¿Debe el router tener en cuenta esta ruta? Si no, prueba con la siguiente.
canMatch actúa antes, mientras el router busca qué ruta coincide con la URL. Si devuelve false, el router hace como si la ruta no existiera y sigue probando las siguientes. Eso permite tener dos rutas con el mismo path para perfiles distintos y evita descargar el código de una ruta diferida que el usuario no puede usar.
Úsalo cuando
Para mostrar componentes distintos en la misma URL según el rol, y para no descargar secciones con loadChildren a quien no tiene permiso.
Ten en cuenta
Si ninguna ruta acepta la URL, acabarás en la comodín ** o en un error de navegación. Y, como con cualquier guard, la seguridad real está en el servidor.
const esAdmin: CanMatchFn = () => inject(Sesion).rol() === "admin";
export const routes: Routes = [
// Misma URL, distinto componente según el rol
{ path: "panel", canMatch: [esAdmin], loadComponent: () => import("./panel-admin") },
{ path: "panel", loadComponent: () => import("./panel-cliente") },
];CanDeactivateFn
¿Puede el usuario salir de esta ruta? Ideal para cambios sin guardar.
canDeactivate se ejecuta al intentar abandonar una ruta y recibe la instancia del componente actual. Así puede preguntarle si tiene cambios pendientes, por ejemplo consultando form.dirty de un formulario reactivo, y pedir confirmación antes de dejar salir al usuario.
Úsalo cuando
En pantallas de edición largas: formularios, editores de texto, configuraciones con botón de guardar.
Ten en cuenta
Solo cubre la navegación dentro de la app. Si el usuario cierra la pestaña o recarga, el router no se entera; para eso existe el evento beforeunload del navegador.
export const sinCambiosPendientes: CanDeactivateFn<EditarPerfil> = (componente) =>
componente.form.dirty
? confirm("Tienes cambios sin guardar. ¿Quieres salir igualmente?")
: true;
// app.routes.ts
{ path: "perfil", component: EditarPerfil, canDeactivate: [sinCambiosPendientes] }Resolvers
Cargar los datos que necesita una ruta antes de mostrarla.
Un resolver es una función de tipo ResolveFn que el router ejecuta después de los guards y espera antes de activar la ruta. Lo que devuelve (un valor, una Promise o un Observable) queda disponible para el componente en ActivatedRoute.data o, con withComponentInputBinding(), como un input con el nombre de su clave.
Úsalo cuando
Cuando la página no tiene sentido sin sus datos, como el detalle de un pedido, y prefieres no pintar una pantalla vacía.
Ten en cuenta
Mientras el resolver espera, el usuario sigue viendo la página anterior sin ninguna señal de progreso: si la petición es lenta, parece que el clic no ha funcionado. Muestra un indicador global o carga los datos en el componente con un estado de «cargando».
export const productoResolver: ResolveFn<Producto> = (ruta) =>
inject(Catalogo).obtener(Number(ruta.paramMap.get("id")));
// app.routes.ts
{
path: "productos/:id",
component: DetalleProducto,
resolve: { producto: productoResolver },
}
// detalle-producto.ts (con withComponentInputBinding)
producto = input.required<Producto>();Navegación programática
Navegar desde el código con Router.navigate y navigateByUrl.
A veces quien navega no es un enlace, sino el código: tras guardar un formulario, al terminar un pago, al cerrar la sesión. Para eso inyectas Router y llamas a navigate(), que recibe un array de segmentos como routerLink y opciones como queryParams o relativeTo, o a navigateByUrl(), que recibe la URL completa como texto.
Las dos devuelven una Promise que se resuelve con true si la navegación termina, o con false si, por ejemplo, un guard la cancela.
Úsalo cuando
navigate() cuando construyes la ruta a partir de datos (['/pedidos', pedido.id]); navigateByUrl() cuando ya tienes la URL entera, como la que guardaste antes de enviar al usuario al login.
Ten en cuenta
No sustituyas los enlaces por botones que navegan: un <button (click)> no se puede abrir en otra pestaña ni guardar en favoritos. Para ir a otra página usa routerLink y reserva navigate() para después de una acción.
export class Login {
private router = inject(Router);
private sesion = inject(Sesion);
volver = input("/"); // ?volver=… con withComponentInputBinding
async entrar(credenciales: Credenciales) {
await this.sesion.iniciar(credenciales);
this.router.navigateByUrl(this.volver()); // ya tienes la URL entera
}
}
// Tras un pago: la ruta se construye por segmentos
this.router.navigate(["/pedidos", pedido.id], { queryParams: { nuevo: 1 } });Atención
Los guards deciden qué ve el usuario, pero no protegen ningún dato: todo el JavaScript de tu app llega al navegador. Si una ruta muestra información privada, la API que la sirve debe comprobar la sesión y los permisos en cada petición.
Interactivo
Guards y resolvers cara a cara
Activa o desactiva opciones para compararlas cara a cara.
| Característica | CanActivateFn | CanMatchFn | CanDeactivateFn |
|---|---|---|---|
| Pregunta que responde | ¿Puede entrar en esta ruta? | ¿Debe tenerse en cuenta esta ruta? | ¿Puede salir de esta ruta? |
| Cuándo se ejecuta | Con la ruta ya encontrada, antes de mostrarla | Mientras el router busca qué ruta coincide | Al intentar abandonar el componente actual |
| Si dice que no | Cancela o redirige con un UrlTree | El router prueba la siguiente ruta | El usuario se queda en la página |
| Uso típico | Zona privada que requiere sesión | Rutas por rol; no descargar código diferido | Cambios sin guardar |
Ponlo en práctica
Saber qué es cada cosa es la mitad del trabajo. La otra mitad es reconocer cuándo usarla.
Interactivo
¿Qué usarías?
Lee cada situación y elige la opción que mejor la resuelve. Fallar aquí no cuenta: es para practicar.
La sección «Mi cuenta» es solo para usuarios con sesión. Los demás deben ir al login y, al entrar, volver a donde querían ir.
Para recordar
- Las rutas son un array
RoutesqueprovideRouteractiva; el componente de la ruta se pinta en<router-outlet />. - Navega con
routerLinken las plantillas y conRouter.navigateonavigateByUrldesde el código, después de una acción. - Los parámetros llegan por
ActivatedRouteo, conwithComponentInputBinding(), directamente como inputs del componente. loadComponentyloadChildrendividen la app en archivos que se descargan al visitar cada sección.- Los guards y los resolvers son funciones que usan
inject(); mejoran la experiencia, pero la seguridad está en el servidor.
Comprueba lo aprendido
Cuestionario
Responde las 5 preguntas para completar la lección.
1.¿Dónde se pinta el componente de la ruta activa?
2.Lees el id con
ruta.snapshot.paramMap.get('id')al crear el componente. El usuario pasa de/productos/1a/productos/2. ¿Qué ocurre?3.¿Por qué la ruta
**debe ir la última de la lista?4.Una sección se carga con
loadChildren. ¿Qué diferencia hay entre protegerla concanMatcho concanActivate?5.¿Qué hace un resolver?