Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

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 is essential for building single-page applications with multiple views. In a traditional multi-page website, clicking a link causes a full page reload — the browser requests a new HTML document from the server. In a Single Page Application (SPA), the router intercepts navigation, swaps out the component displayed on screen, and updates the browser URL — all without a page reload. The result feels instant, like a native app. The Angular Router handles this entire orchestration: matching URLs to components, running security checks (guards), pre-fetching data (resolvers), and managing browser history. 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. Lazy loading is one of the highest-impact performance optimizations in Angular. Without it, your entire application — every feature, every admin page, every settings screen — gets downloaded upfront before the user sees anything. With lazy loading, the browser only downloads the code for the page the user is actually visiting. Additional pages load on demand as the user navigates.
// 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. Guards are the bouncers at the door of your routes — they decide who gets in and who gets redirected. Angular runs guards before the route activates, so the user never sees a flash of unauthorized content.
Common pitfall: Never rely on route guards as your only security layer. Guards run on the client and can be bypassed by a determined user. Always verify permissions on the server side too. Guards are a UX convenience (redirecting unauthorized users to login), not a security mechanism.

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

Interview Deep-Dive

Strong Answer: This is a classic Angular routing issue related to component reuse and data fetching. By default, when you navigate to /products/1 and then /products/2, Angular reuses the ProductDetailComponent instance because the route template is the same (products/:id). The component’s ngOnInit does not re-run because the component is not destroyed and recreated.The fix depends on how you fetch data. If you use route.snapshot.params, you only get the params at the time of the initial navigation — they do not update. You need to subscribe to route.params (an observable) or use signal-based input binding (withComponentInputBinding) to react to parameter changes. With input binding, the id signal input updates automatically when the route changes.For the back button specifically, the issue is compounded because the browser restores the scroll position and previous URL, but Angular’s route resolver or data-fetching logic might not re-trigger. If using resolvers, the data is re-fetched on each navigation. If fetching in the component, you need the reactive params pattern.I would also check if the app uses onSameUrlNavigation: ‘reload’ in the router config, which controls whether navigating to the same URL triggers route processing. And I would verify that any caching layer (shareReplay, service-level cache) invalidates correctly.Follow-up: How does withComponentInputBinding change the way you handle route parameters? Answer: withComponentInputBinding automatically maps route parameters, query parameters, and resolved data to component inputs with matching names. So if your route has :id and your component has id = input.required<string>(), the value is automatically provided. This eliminates the need to inject ActivatedRoute entirely. The signal input updates reactively when the route changes, so you can use computed() or effect() to react to parameter changes — no manual subscription needed.
Strong Answer: canActivate runs after route matching and prevents navigation to a route — the user sees a redirect or error page. canDeactivate runs when leaving a route and can prevent navigation away — the classic “unsaved changes” confirmation. canMatch runs during route matching itself and affects which route definition is selected.The critical difference with canMatch: it controls route matching, not just access. If canMatch returns false, the router does not just block access — it acts as if that route definition does not exist and continues checking the next route definition with the same path.The perfect real-world scenario: role-based dashboards. You have an admin dashboard and a user dashboard, both at /dashboard. With canMatch, you define two route entries for the same path, each with a different canMatch guard checking the user’s role. When an admin navigates to /dashboard, canMatch on the admin route returns true, so they get AdminDashboardComponent. When a regular user navigates to /dashboard, canMatch on the admin route returns false, the router skips it, and matches the next route definition which loads UserDashboardComponent.You cannot do this with canActivate because canActivate runs after matching — it would match the first /dashboard route and then redirect, which changes the URL and creates a worse UX.Follow-up: Should you rely on route guards as a security mechanism? Answer: Never as the sole mechanism. Route guards are a UX convenience layer. They prevent honest users from accidentally landing on pages they should not see, and they create smooth redirect-to-login flows. But guards run entirely on the client — a determined user can open DevTools, modify the guard’s return value, or directly call your API endpoints. Every authorization check must be enforced server-side. The guard prevents seeing a flash of the admin page; the API prevents actually accessing admin data.
Strong Answer: My approach: every feature that is not on the initial landing page gets lazy loaded. This means each feature has its own routes file exported as a constant, and the app.routes.ts uses loadChildren to point to it. The initial bundle only contains the shell (nav, footer), the landing page component, and the core services.For preloading strategy, I would not use PreloadAllModules blindly because it downloads everything immediately after initial load — fine for small apps, wasteful for large ones. Instead, I implement a custom PreloadingStrategy that prioritizes based on likelihood of navigation. For example, if analytics show 80% of users visit the product catalog after landing, I preload that first. Admin modules only preload if the user has an admin role.The implementation: create a class that implements PreloadingStrategy and its preload method. For each route, check a condition (role, analytics data, or a custom route data property like preload: true) and either call load() to preload or return EMPTY to skip.I also use @defer with prefetch on idle for component-level lazy loading within pages. So a product detail page lazy-loads its review section and recommendation carousel independently.The measurable impact: on a project I worked on, we reduced initial bundle from 2.1MB to 380KB by lazy loading features, and then reduced perceived load time further with strategic preloading so navigations to likely-next pages were instant.Follow-up: How do you verify that lazy loading is actually working and not accidentally pulling modules into the main bundle? Answer: I use the Angular CLI’s ng build —stats-json flag to generate a webpack stats file, then visualize it with webpack-bundle-analyzer or source-map-explorer. These tools show exactly which modules are in which chunk. If a lazy module shows up in the main bundle, it means something in the main bundle imports from it directly — usually a shared barrel export (index.ts) that re-exports everything.

Next Steps

Next: Angular Forms

Master template-driven and reactive forms for user input