En esta página

Routing avanzado: guards, resolvers y layouts

15 min lectura TextoCap. 3 — Arquitectura

Sistema de routing en Angular

El router de Angular mapea URLs a componentes. En Angular 21, las rutas se definen como un array de objetos Route en un archivo dedicado.

Configurar el router

En app.config.ts, provee el router con las opciones necesarias:

import { ApplicationConfig } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(
      routes,
      withComponentInputBinding(),  // Parametros como inputs
    ),
  ],
};

Lazy loading

El lazy loading carga componentes solo cuando el usuario navega a su ruta:

// Un solo componente
{
  path: 'about',
  loadComponent: () => import('./about').then(m => m.About),
}

// Grupo de rutas (archivo separado)
{
  path: 'admin',
  loadChildren: () => import('./admin/routes').then(m => m.adminRoutes),
}

Parametros de ruta

Con withComponentInputBinding(), los parametros de ruta se inyectan como inputs:

// Ruta: /cursos/:slug
@Component({ /* ... */ })
export class CursoDetalle {
  // El router inyecta el valor del parametro :slug
  readonly slug = input.required<string>();
}

También puedes acceder a query params:

// URL: /buscar?q=angular
@Component({ /* ... */ })
export class Buscar {
  readonly q = input<string>('');  // query param "q"
}

Guards funcionales

Los guards protegen rutas. En Angular 21 se implementan como funciones simples:

function authGuard() {
  const auth = inject(AuthService);
  const router = inject(Router);

  if (auth.estaAutenticado()) return true;
  return router.createUrlTree(['/login']);
}

// Uso en la ruta
{ path: 'dashboard', canActivate: [authGuard], /* ... */ }

Tipos de guards disponibles:

Guard Cuando se ejecuta
canActivate Antes de activar la ruta
canDeactivate Antes de salir de la ruta
canMatch Antes de intentar coincidir la ruta
resolve Antes de renderizar, para pre-cargar datos

Resolvers

Los resolvers cargan datos antes de que el componente se renderice:

function cursoResolver(route: ActivatedRouteSnapshot) {
  const cursos = inject(CursosService);
  const slug = route.paramMap.get('slug')!;
  return cursos.obtenerPorSlug(slug);
}

// En la ruta
{
  path: ':slug',
  loadComponent: () => import('./detalle').then(m => m.Detalle),
  resolve: { curso: cursoResolver },
}

El dato resuelto se inyecta como input en el componente:

@Component({ /* ... */ })
export class Detalle {
  readonly curso = input.required<Curso>();  // Inyectado por el resolver
}

Layouts anidados

Usa <router-outlet> dentro de un componente layout para crear jerarquias:

// dashboard-layout.ts
@Component({
  selector: 'app-dashboard-layout',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [RouterOutlet, RouterLink, RouterLinkActive],
  template: `
    <aside><nav>...</nav></aside>
    <main><router-outlet /></main>
  `,
})
export class DashboardLayout {}

routerLinkActive

Agrega una clase CSS cuando el link coincide con la ruta activa:

<a routerLink="/cursos" routerLinkActive="activo">Cursos</a>

Práctica

  1. Configura lazy loading: Crea dos rutas con loadComponent que carguen componentes de forma perezosa. Verifica en DevTools (Network) que los chunks se cargan solo al navegar.
  2. Implementa un guard funcional: Crea un authGuard que verifique si un signal estaAutenticado es true. Si no lo es, redirige a /login usando router.createUrlTree().
  3. Crea un layout anidado: Implementa un componente layout con sidebar y <router-outlet>. Define rutas hijas que se rendericen dentro del layout y usa routerLinkActive para resaltar el enlace activo.

En la siguiente leccion aprenderemos sobre formularios reactivos para manejar entradas de usuario con validación.

Lazy loading siempre
Usa loadComponent para rutas individuales y loadChildren para grupos de rutas. Esto divide el bundle en chunks que se cargan bajo demanda, reduciendo el tiempo de carga inicial.
withComponentInputBinding
Configura provideRouter(routes, withComponentInputBinding()) en tu app.config para que los parametros de ruta se inyecten automaticamente como inputs del componente. Así no necesitas ActivatedRoute.
import { ActivatedRouteSnapshot, Router, Routes } from '@angular/router';
import { inject } from '@angular/core';
import { AuthService } from './auth.service';

// --- Guard funcional ---
function authGuard() {
  const auth = inject(AuthService);
  const router = inject(Router);
  if (auth.estaAutenticado()) return true;
  return router.createUrlTree(['/login']);
}

function adminGuard() {
  const auth = inject(AuthService);
  return auth.esAdmin();
}

// --- Resolver funcional ---
function cursoResolver(route: ActivatedRouteSnapshot) {
  const cursos = inject(CursosService);
  return cursos.obtenerPorSlug(route.paramMap.get('slug')!);
}

// --- Rutas de la app ---
export const routes: Routes = [
  {
    path: '',
    loadComponent: () => import('./home').then(m => m.Home),
  },
  {
    path: 'cursos',
    children: [
      {
        path: '',
        loadComponent: () =>
          import('./catálogo').then(m => m.Catalogo),
      },
      {
        path: ':slug',
        loadComponent: () =>
          import('./curso-detalle').then(m => m.CursoDetalle),
        resolve: { curso: cursoResolver },
      },
    ],
  },
  {
    path: 'dashboard',
    canActivate: [authGuard],
    loadComponent: () =>
      import('./dashboard/layout').then(m => m.DashboardLayout),
    children: [
      { path: '', redirectTo: 'resumen', pathMatch: 'full' },
      {
        path: 'resumen',
        loadComponent: () =>
          import('./dashboard/resumen').then(m => m.Resumen),
      },
      {
        path: 'progreso',
        loadComponent: () =>
          import('./dashboard/progreso').then(m => m.Progreso),
      },
    ],
  },
  {
    path: 'admin',
    canActivate: [authGuard, adminGuard],
    loadChildren: () =>
      import('./admin/admin.routes').then(m => m.adminRoutes),
  },
  { path: '**', redirectTo: '' },
];