Skip to main content
Angular Routing

Module Overview

Estimated Time: 3-4 hours | Difficulty: Intermediate | Prerequisites: Module 5
Angular Router enables navigation between views, supports deep linking, lazy loading, and provides guards for protecting routes. It’s essential for building single-page applications with multiple views. What You’ll Learn:
  • Setting up routing configuration
  • Router outlet and navigation
  • Route parameters and query params
  • Lazy loading for performance
  • Route guards and resolvers
  • Nested routes and layouts

Router Architecture

┌─────────────────────────────────────────────────────────────────────────┐
│                    Angular Router Architecture                           │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   URL: /products/123/details?sort=price#reviews                         │
│         ├───────┬───┬───────┬─────────────┬────────                     │
│         │       │   │       │             │                              │
│         ▼       ▼   ▼       ▼             ▼                              │
│       path   param  child   query       fragment                        │
│                                                                          │
│   ┌─────────────────────────────────────────────────────────────────┐   │
│   │                         Router                                   │   │
│   │  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────────────┐│   │
│   │  │  Guards  │──│ Resolver │──│  Outlet  │──│    Component     ││   │
│   │  └──────────┘  └──────────┘  └──────────┘  └──────────────────┘│   │
│   │       │             │             │                             │   │
│   │       ▼             ▼             ▼                             │   │
│   │   Can navigate?  Fetch data   Render here                       │   │
│   └─────────────────────────────────────────────────────────────────┘   │
│                                                                          │
│   Lifecycle: URL Change → Match Route → Run Guards → Resolve Data       │
│              → Activate Route → Render Component                        │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Basic Routing Setup

Configure Routes

// app.routes.ts
import { Routes } from '@angular/router';

export const routes: Routes = [
  { path: '', redirectTo: '/home', pathMatch: 'full' },
  { path: 'home', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: 'products', component: ProductsComponent },
  { path: 'products/:id', component: ProductDetailComponent },
  { path: '**', component: NotFoundComponent }  // Wildcard for 404
];
// 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)
  ]
};

Router Outlet

// app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet, RouterLink, RouterLinkActive } from '@angular/router';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, RouterLink, RouterLinkActive],
  template: `
    <nav>
      <a routerLink="/home" routerLinkActive="active">Home</a>
      <a routerLink="/about" routerLinkActive="active">About</a>
      <a routerLink="/products" routerLinkActive="active">Products</a>
    </nav>
    
    <!-- Route content renders here -->
    <main>
      <router-outlet />
    </main>
    
    <footer>© 2024 My App</footer>
  `,
  styles: [`
    .active {
      font-weight: bold;
      color: #DD0031;
    }
  `]
})
export class AppComponent {}

Route Parameters

Path Parameters

// products/:id route
import { Component, inject, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-product-detail',
  standalone: true,
  template: `
    <h1>Product {{ productId }}</h1>
    <!-- Product details -->
  `
})
export class ProductDetailComponent implements OnInit {
  private route = inject(ActivatedRoute);
  productId!: string;
  
  ngOnInit() {
    // Snapshot (for non-changing params)
    this.productId = this.route.snapshot.params['id'];
    
    // Observable (for params that might change)
    this.route.params.subscribe(params => {
      this.productId = params['id'];
      this.loadProduct();
    });
  }
}

Modern: Input Binding for Route Parameters

// app.config.ts
import { provideRouter, withComponentInputBinding } from '@angular/router';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes, withComponentInputBinding())  // Enable input binding
  ]
};
// product-detail.component.ts
@Component({
  selector: 'app-product-detail',
  standalone: true,
  template: `<h1>Product {{ id }}</h1>`
})
export class ProductDetailComponent {
  // Route param automatically bound as input!
  id = input.required<string>();
  
  // Also works with query params
  sort = input<string>();  // ?sort=price
}

Query Parameters

@Component({
  selector: 'app-products',
  standalone: true,
  imports: [RouterLink],
  template: `
    <!-- Navigate with query params -->
    <a [routerLink]="['/products']" [queryParams]="{ category: 'electronics', sort: 'price' }">
      Electronics
    </a>
    
    <button (click)="applyFilters()">Apply Filters</button>
  `
})
export class ProductsComponent {
  private route = inject(ActivatedRoute);
  private router = inject(Router);
  
  ngOnInit() {
    // Read query params
    this.route.queryParams.subscribe(params => {
      console.log(params['category'], params['sort']);
    });
  }
  
  applyFilters() {
    // Navigate with query params programmatically
    this.router.navigate(['/products'], {
      queryParams: { category: 'books', sort: 'rating' },
      queryParamsHandling: 'merge'  // Preserve existing params
    });
  }
}

Programmatic Navigation

import { Router } from '@angular/router';

@Component({...})
export class ProductComponent {
  private router = inject(Router);
  
  // Simple navigation
  goHome() {
    this.router.navigate(['/home']);
  }
  
  // With route params
  viewProduct(id: number) {
    this.router.navigate(['/products', id]);
  }
  
  // With query params
  searchProducts(term: string) {
    this.router.navigate(['/products'], {
      queryParams: { search: term }
    });
  }
  
  // Relative navigation
  goToDetails() {
    this.router.navigate(['details'], { relativeTo: this.route });
  }
  
  // Replace current URL (no back button entry)
  login() {
    this.router.navigate(['/dashboard'], { replaceUrl: true });
  }
  
  // Navigate with state
  editProduct(product: Product) {
    this.router.navigate(['/products', product.id, 'edit'], {
      state: { product }  // Pass data without URL
    });
  }
}

Lazy Loading

Load feature modules only when needed:
// app.routes.ts
export const routes: Routes = [
  { path: '', redirectTo: '/home', pathMatch: 'full' },
  { path: 'home', component: HomeComponent },
  
  // Lazy load entire feature
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.routes')
      .then(m => m.ADMIN_ROUTES)
  },
  
  // Lazy load single component
  {
    path: 'settings',
    loadComponent: () => import('./settings/settings.component')
      .then(m => m.SettingsComponent)
  }
];
// admin/admin.routes.ts
export const ADMIN_ROUTES: Routes = [
  { path: '', component: AdminDashboardComponent },
  { path: 'users', component: AdminUsersComponent },
  { path: 'settings', component: AdminSettingsComponent }
];
┌─────────────────────────────────────────────────────────────────────────┐
│                    Lazy Loading Benefits                                 │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   WITHOUT Lazy Loading                WITH Lazy Loading                  │
│   ─────────────────────              ──────────────────                  │
│                                                                          │
│   Initial Bundle: 2MB                 Initial Bundle: 500KB              │
│   ┌─────────────────┐                 ┌──────────────────┐               │
│   │  App + Admin +  │                 │      App         │               │
│   │  Products +     │                 │    (core only)   │               │
│   │  Settings       │                 └──────────────────┘               │
│   └─────────────────┘                          +                         │
│                                       ┌─────────┐ ┌─────────┐           │
│   Load time: 3-5 seconds              │  Admin  │ │Products │           │
│                                       │ (on nav)│ │(on nav) │           │
│                                       └─────────┘ └─────────┘           │
│                                                                          │
│                                       Initial load: 1-2 seconds          │
│                                       Feature loads: ~200ms each         │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Route Guards

Protect routes with functional guards:

canActivate - Protect Route Access

// guards/auth.guard.ts
import { inject } from '@angular/core';
import { Router, CanActivateFn } from '@angular/router';
import { AuthService } from '../services/auth.service';

export const authGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);
  
  if (authService.isAuthenticated()) {
    return true;
  }
  
  // Redirect to login with return URL
  return router.createUrlTree(['/login'], {
    queryParams: { returnUrl: state.url }
  });
};
// app.routes.ts
export const routes: Routes = [
  { path: 'login', component: LoginComponent },
  {
    path: 'dashboard',
    component: DashboardComponent,
    canActivate: [authGuard]
  },
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.routes').then(m => m.ADMIN_ROUTES),
    canActivate: [authGuard, adminGuard]  // Multiple guards
  }
];

canDeactivate - Confirm Before Leaving

// guards/unsaved-changes.guard.ts
export interface CanDeactivateComponent {
  canDeactivate: () => boolean | Observable<boolean>;
}

export const unsavedChangesGuard: CanDeactivateFn<CanDeactivateComponent> = 
  (component, currentRoute, currentState, nextState) => {
    if (component.canDeactivate && !component.canDeactivate()) {
      return confirm('You have unsaved changes. Leave anyway?');
    }
    return true;
  };
// edit-profile.component.ts
@Component({...})
export class EditProfileComponent implements CanDeactivateComponent {
  hasUnsavedChanges = signal(false);
  
  canDeactivate(): boolean {
    return !this.hasUnsavedChanges();
  }
}

canMatch - Control Route Matching

// Role-based route matching
export const roleGuard = (requiredRole: string): CanMatchFn => {
  return (route, segments) => {
    const authService = inject(AuthService);
    return authService.hasRole(requiredRole);
  };
};

// Different components based on role
export const routes: Routes = [
  {
    path: 'dashboard',
    component: AdminDashboardComponent,
    canMatch: [roleGuard('admin')]
  },
  {
    path: 'dashboard',
    component: UserDashboardComponent,
    canMatch: [roleGuard('user')]
  }
];

Route Resolvers

Pre-fetch data before activating route:
// resolvers/product.resolver.ts
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { ProductService } from '../services/product.service';
import { Product } from '../models/product.model';

export const productResolver: ResolveFn<Product> = (route, state) => {
  const productService = inject(ProductService);
  const productId = route.params['id'];
  
  return productService.getProduct(productId);
};
// app.routes.ts
export const routes: Routes = [
  {
    path: 'products/:id',
    component: ProductDetailComponent,
    resolve: {
      product: productResolver
    }
  }
];
// product-detail.component.ts
@Component({
  selector: 'app-product-detail',
  template: `
    <h1>{{ product().name }}</h1>
    <p>{{ product().description }}</p>
  `
})
export class ProductDetailComponent {
  private route = inject(ActivatedRoute);
  
  // Data is already loaded!
  product = toSignal(
    this.route.data.pipe(map(data => data['product'])),
    { requireSync: true }
  );
}

Nested Routes & Layouts

// app.routes.ts
export const routes: Routes = [
  {
    path: 'admin',
    component: AdminLayoutComponent,  // Layout with its own router-outlet
    canActivate: [authGuard, adminGuard],
    children: [
      { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
      { path: 'dashboard', component: AdminDashboardComponent },
      { path: 'users', component: AdminUsersComponent },
      { 
        path: 'users/:id',
        component: AdminUserDetailComponent,
        children: [
          { path: 'profile', component: UserProfileComponent },
          { path: 'permissions', component: UserPermissionsComponent }
        ]
      },
      { path: 'settings', component: AdminSettingsComponent }
    ]
  }
];
// admin-layout.component.ts
@Component({
  selector: 'app-admin-layout',
  standalone: true,
  imports: [RouterOutlet, RouterLink, RouterLinkActive],
  template: `
    <div class="admin-layout">
      <aside class="sidebar">
        <nav>
          <a routerLink="dashboard" routerLinkActive="active">Dashboard</a>
          <a routerLink="users" routerLinkActive="active">Users</a>
          <a routerLink="settings" routerLinkActive="active">Settings</a>
        </nav>
      </aside>
      
      <main class="content">
        <!-- Nested routes render here -->
        <router-outlet />
      </main>
    </div>
  `
})
export class AdminLayoutComponent {}
┌─────────────────────────────────────────────────────────────────────────┐
│   URL: /admin/users/123/permissions                                      │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   ┌───────────────────────────────────────────────────────────────────┐ │
│   │  AppComponent (router-outlet)                                      │ │
│   │  ┌───────────────────────────────────────────────────────────────┐│ │
│   │  │  AdminLayoutComponent (router-outlet)                         ││ │
│   │  │  ┌───────────────────────────────────────────────────────────┐││ │
│   │  │  │  AdminUserDetailComponent (router-outlet)                 │││ │
│   │  │  │  ┌───────────────────────────────────────────────────────┐│││ │
│   │  │  │  │  UserPermissionsComponent                             ││││ │
│   │  │  │  └───────────────────────────────────────────────────────┘│││ │
│   │  │  └───────────────────────────────────────────────────────────┘││ │
│   │  └───────────────────────────────────────────────────────────────┘│ │
│   └───────────────────────────────────────────────────────────────────┘ │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Router Events

Monitor navigation lifecycle:
import { Router, NavigationStart, NavigationEnd, NavigationError } from '@angular/router';

@Component({...})
export class AppComponent {
  private router = inject(Router);
  isLoading = signal(false);
  
  constructor() {
    this.router.events.subscribe(event => {
      if (event instanceof NavigationStart) {
        this.isLoading.set(true);
      }
      
      if (event instanceof NavigationEnd) {
        this.isLoading.set(false);
        // Analytics tracking
        this.analytics.trackPageView(event.urlAfterRedirects);
      }
      
      if (event instanceof NavigationError) {
        this.isLoading.set(false);
        console.error('Navigation error:', event.error);
      }
    });
  }
}

Router Configuration Options

// app.config.ts
import { 
  provideRouter, 
  withComponentInputBinding,
  withViewTransitions,
  withPreloading,
  PreloadAllModules
} from '@angular/router';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(
      routes,
      withComponentInputBinding(),     // Bind route params to inputs
      withViewTransitions(),           // Animate route transitions
      withPreloading(PreloadAllModules), // Preload lazy modules
      withRouterConfig({
        onSameUrlNavigation: 'reload',  // Reload on same URL navigation
        paramsInheritanceStrategy: 'always'  // Inherit params from parent
      })
    )
  ]
};

Practice Exercise

Exercise: Build a Product Catalog with Routing

Create a multi-page app with:
  1. Product list page with category filters via query params
  2. Product detail page with tabs (info, reviews, specs)
  3. Auth guard protecting checkout route
  4. Lazy loaded admin section
  5. 404 page for unknown routes
// app.routes.ts
export const routes: Routes = [
  { path: '', redirectTo: 'products', pathMatch: 'full' },
  { path: 'products', component: ProductListComponent },
  {
    path: 'products/:id',
    component: ProductDetailComponent,
    children: [
      { path: '', redirectTo: 'info', pathMatch: 'full' },
      { path: 'info', component: ProductInfoComponent },
      { path: 'reviews', component: ProductReviewsComponent },
      { path: 'specs', component: ProductSpecsComponent }
    ]
  },
  {
    path: 'checkout',
    loadComponent: () => import('./checkout/checkout.component')
      .then(m => m.CheckoutComponent),
    canActivate: [authGuard]
  },
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.routes')
      .then(m => m.ADMIN_ROUTES),
    canActivate: [authGuard, adminGuard]
  },
  { path: '**', component: NotFoundComponent }
];

// product-list.component.ts
@Component({
  selector: 'app-product-list',
  standalone: true,
  imports: [RouterLink],
  template: `
    <div class="filters">
      @for (category of categories; track category) {
        <button 
          (click)="filterByCategory(category)"
          [class.active]="selectedCategory() === category"
        >
          {{ category }}
        </button>
      }
    </div>
    
    <div class="products">
      @for (product of filteredProducts(); track product.id) {
        <a [routerLink]="['/products', product.id]" class="product-card">
          <h3>{{ product.name }}</h3>
          <p>{{ product.price | currency }}</p>
        </a>
      }
    </div>
  `
})
export class ProductListComponent {
  private route = inject(ActivatedRoute);
  private router = inject(Router);
  private productService = inject(ProductService);
  
  categories = ['All', 'Electronics', 'Clothing', 'Books'];
  products = toSignal(this.productService.getProducts(), { initialValue: [] });
  
  selectedCategory = toSignal(
    this.route.queryParams.pipe(map(p => p['category'] || 'All')),
    { initialValue: 'All' }
  );
  
  filteredProducts = computed(() => {
    const category = this.selectedCategory();
    if (category === 'All') return this.products();
    return this.products().filter(p => p.category === category);
  });
  
  filterByCategory(category: string) {
    this.router.navigate([], {
      queryParams: { category: category === 'All' ? null : category },
      queryParamsHandling: 'merge'
    });
  }
}

// product-detail.component.ts
@Component({
  selector: 'app-product-detail',
  standalone: true,
  imports: [RouterOutlet, RouterLink, RouterLinkActive],
  template: `
    <div class="product-detail">
      <h1>{{ product()?.name }}</h1>
      
      <nav class="tabs">
        <a routerLink="info" routerLinkActive="active">Info</a>
        <a routerLink="reviews" routerLinkActive="active">Reviews</a>
        <a routerLink="specs" routerLinkActive="active">Specs</a>
      </nav>
      
      <div class="tab-content">
        <router-outlet />
      </div>
    </div>
  `
})
export class ProductDetailComponent {
  id = input.required<string>();
  private productService = inject(ProductService);
  
  product = computed(() => 
    this.productService.getProductById(+this.id())
  );
}

Summary

1

Route Configuration

Define routes with path, component, and optional guards/resolvers
2

Navigation

Use RouterLink in templates or Router.navigate() programmatically
3

Lazy Loading

Use loadChildren/loadComponent for better initial load performance
4

Guards

Protect routes with canActivate, canDeactivate, canMatch
5

Resolvers

Pre-fetch data before route activation

Next Steps

Next: Angular Forms

Master template-driven and reactive forms for user input