On this page

Advanced routing: guards, resolvers, and layouts

15 min read TextCh. 3 — Architecture

Angular's routing system

Angular's router maps URLs to components. In Angular 21, routes are defined as an array of Route objects in a dedicated file.

Configuring the router

In app.config.ts, provide the router with the necessary options:

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

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

Lazy loading

Lazy loading loads components only when the user navigates to their route:

// A single component
{
  path: 'about',
  loadComponent: () => import('./about').then(m => m.About),
}

// Route group (separate file)
{
  path: 'admin',
  loadChildren: () => import('./admin/routes').then(m => m.adminRoutes),
}

Route parameters

With withComponentInputBinding(), route parameters are injected as inputs:

// Route: /courses/:slug
@Component({ /* ... */ })
export class CourseDetail {
  // The router injects the :slug parameter value
  readonly slug = input.required<string>();
}

You can also access query params:

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

Functional guards

Guards protect routes. In Angular 21, they are implemented as simple functions:

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

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

// Usage in the route
{ path: 'dashboard', canActivate: [authGuard], /* ... */ }

Available guard types:

Guard When it runs
canActivate Before activating the route
canDeactivate Before leaving the route
canMatch Before attempting to match the route
resolve Before rendering, to pre-load data

Resolvers

Resolvers load data before the component renders:

function courseResolver(route: ActivatedRouteSnapshot) {
  const courses = inject(CoursesService);
  const slug = route.paramMap.get('slug')!;
  return courses.getBySlug(slug);
}

// In the route
{
  path: ':slug',
  loadComponent: () => import('./detail').then(m => m.Detail),
  resolve: { course: courseResolver },
}

The resolved data is injected as an input in the component:

@Component({ /* ... */ })
export class Detail {
  readonly course = input.required<Course>();  // Injected by the resolver
}

Nested layouts

Use <router-outlet> inside a layout component to create hierarchies:

// 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

Adds a CSS class when the link matches the active route:

<a routerLink="/courses" routerLinkActive="active">Courses</a>

Practice

  1. Set up lazy loading: Create two routes with loadComponent that lazily load components. Verify in DevTools (Network) that chunks load only when navigating.
  2. Implement a functional guard: Create an authGuard that checks if an isAuthenticated signal is true. If not, redirect to /login using router.createUrlTree().
  3. Build a nested layout: Implement a layout component with a sidebar and <router-outlet>. Define child routes that render inside the layout and use routerLinkActive to highlight the active link.

In the next lesson, we will learn about reactive forms for handling user input with validation.

Always lazy load
Use loadComponent for individual routes and loadChildren for route groups. This splits the bundle into chunks that are loaded on demand, reducing the initial load time.
withComponentInputBinding
Configure provideRouter(routes, withComponentInputBinding()) in your app.config so that route parameters are automatically injected as component inputs. This way you don't need ActivatedRoute.
import { ActivatedRouteSnapshot, Router, Routes } from '@angular/router';
import { inject } from '@angular/core';
import { AuthService } from './auth.service';

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

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

// --- Functional resolver ---
function courseResolver(route: ActivatedRouteSnapshot) {
  const courses = inject(CoursesService);
  return courses.getBySlug(route.paramMap.get('slug')!);
}

// --- App routes ---
export const routes: Routes = [
  {
    path: '',
    loadComponent: () => import('./home').then(m => m.Home),
  },
  {
    path: 'courses',
    children: [
      {
        path: '',
        loadComponent: () =>
          import('./catalog').then(m => m.Catalog),
      },
      {
        path: ':slug',
        loadComponent: () =>
          import('./course-detail').then(m => m.CourseDetail),
        resolve: { course: courseResolver },
      },
    ],
  },
  {
    path: 'dashboard',
    canActivate: [authGuard],
    loadComponent: () =>
      import('./dashboard/layout').then(m => m.DashboardLayout),
    children: [
      { path: '', redirectTo: 'overview', pathMatch: 'full' },
      {
        path: 'overview',
        loadComponent: () =>
          import('./dashboard/overview').then(m => m.Overview),
      },
      {
        path: 'progress',
        loadComponent: () =>
          import('./dashboard/progress').then(m => m.Progress),
      },
    ],
  },
  {
    path: 'admin',
    canActivate: [authGuard, adminGuard],
    loadChildren: () =>
      import('./admin/admin.routes').then(m => m.adminRoutes),
  },
  { path: '**', redirectTo: '' },
];