Intermediate

Angular: Routing and Navigation

Routing from basic config to params, nested outlets, lazy loading, guards and router state.

Complete guide to Angular’s routing system — from basic configuration to guards, lazy loading, nested routes, and state management via the router.


Table of Contents

  1. Angular Router Overview
  2. Module 2 — Basic Configuration and Navigation
  3. Module 3 — Route Parameters and Query Parameters
  4. Module 4 — Matrix Parameters, Advanced Query Parameters, and Scrolling
  5. Module 5 — Nested and Named RouterOutlets
  6. Module 6 — Lazy Loading
  7. Module 7 — Route Guards
  8. Module 8 — State via the Router (title, data, resolve, providers)
  9. Quick Reference

1. Angular Router Overview

General Architecture

flowchart TD
    Browser["Browser (URL change)"]
    Router["Angular Router"]
    Config["Routes Config\n(app.routes.ts)"]
    Guards["Guards\n(CanActivate / CanMatch\nCanDeactivate)"]
    Resolvers["Resolvers\n(ResolveFn)"]
    Outlet["RouterOutlet\n(primary / named / nested)"]
    Component["Activated Component"]

    Browser --> Router
    Router --> Config
    Config --> Guards
    Guards -->|"true"| Resolvers
    Guards -->|"false / UrlTree"| Browser
    Resolvers --> Outlet
    Outlet --> Component
sequenceDiagram
    participant U as User
    participant R as Router
    participant CM as CanMatch Guard
    participant CA as CanActivate Guard
    participant Res as Resolver
    participant Out as RouterOutlet

    U->>R: Click RouterLink / router.navigate()
    R->>R: NavigationStart event
    R->>CM: Check CanMatch (filters route candidates)
    CM-->>R: true / false
    R->>CA: Check CanActivate
    CA-->>R: true / UrlTree / RedirectCommand
    R->>Res: Execute ResolveFns
    Res-->>R: Resolved data → ActivatedRoute.data
    R->>Out: Activate component in RouterOutlet
    R->>R: NavigationEnd event
    Out-->>U: Rendered view

Initial Configuration — app.config.ts

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

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(
      routes,
      withComponentInputBinding(),       // binds route params to @Input()
      withInMemoryScrolling({
        scrollPositionRestoration: 'enabled'  // scroll to top on navigation
      }),
      withPreloading(PreloadAllModules),  // preloads lazy chunks after initial bundle
      withViewTransitions({ skipInitialTransition: true }) // CSS transitions between views
    ),
    provideHttpClient(),
  ],
};

2. Module 2 — Basic Configuration and Navigation

Route Declarations

Routes are declared in a Routes array. The router traverses this array in order and stops at the first match.

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

export const HOME_ROUTE = 'home';
export const PRODUCTS_ROUTE = 'products';

export const routes: Routes = [
  {
    path: '',
    redirectTo: HOME_ROUTE,
    pathMatch: 'full',
  },
  {
    path: HOME_ROUTE,
    component: HomeComponent,
  },
  {
    path: PRODUCTS_ROUTE,
    children: [
      {
        path: '',
        component: AllProductsComponent,
      },
      {
        path: 'detail',
        component: ProductDetailComponent,
      },
    ],
  },
  {
    path: '**',         // wildcard — must be last
    component: NotFoundComponent,
  },
];
<!-- app.html -->
<app-site-header />
<router-outlet />  <!-- Active route component renders here -->

<!-- navigation in a component -->
<a [routerLink]="['/', HOME_ROUTE]">Home</a>
<a routerLink="/products">Products</a>
<a [routerLink]="[PRODUCTS_ROUTE, 'detail']"
   [queryParams]="{ itemId: item.id }"
   routerLinkActive="active-link">Detail</a>

Programmatic Navigation

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

@Component({...})
export class HeaderComponent {
  private router = inject(Router);

  goToCart() {
    this.router.navigate([CART_ROUTE]);
  }

  goToProduct(id: string) {
    this.router.navigateByUrl(`/products/detail?itemId=${id}`);
  }
}

Router Events

import { Router, NavigationStart, NavigationEnd } from '@angular/router';

constructor(private router: Router) {
  this.router.events.subscribe(event => {
    if (event instanceof NavigationStart) {
      console.log('Navigation started to:', event.url);
    }
    if (event instanceof NavigationEnd) {
      console.log('Navigation ended:', event.urlAfterRedirects);
    }
  });
}

3. Module 3 — Route Parameters and Query Parameters

Parameter Types

graph LR
    URL["URL\n/products/detail;categoryId=pies?itemId=123#section"]
    RP["Route Parameters\n:id  →  snapshot.params['id']"]
    MP["Matrix Parameters\n;key=val  →  snapshot.params['key']"]
    QP["Query Parameters\n?key=val  →  snapshot.queryParams['key']"]
    FR["Fragment\n#anchor  →  snapshot.fragment"]

    URL --> RP
    URL --> MP
    URL --> QP
    URL --> FR

Route Parameters — Parameters in Path

// Declaration
{ path: 'products/:id', component: ProductDetailComponent }

// In the component — with withComponentInputBinding
@Component({...})
export class ProductDetailComponent {
  id = input<string>();  // automatically bound to :id parameter
}

// Without withComponentInputBinding
export class ProductDetailComponent implements OnInit {
  private route = inject(ActivatedRoute);

  ngOnInit() {
    // Snapshot (value at activation time)
    const id = this.route.snapshot.paramMap.get('id');

    // Observable (updates if parameter changes without recreating component)
    this.route.paramMap.subscribe(params => {
      const id = params.get('id');
    });
  }
}

Query Parameters

<a [routerLink]="[PRODUCTS_ROUTE, 'detail']"
   [queryParams]="{ itemId: item.id }"
   queryParamsHandling="merge">
  View Detail
</a>
this.router.navigate([DETAIL_ROUTE], {
  queryParams: { itemId: selectedItem.id },
  queryParamsHandling: 'merge',
  relativeTo: this.activatedRoute,
});
QueryParamsHandlingBehavior
'replace' (default)Replaces all existing query params
'merge'Combines new with existing
'preserve'Keeps existing unchanged

4. Module 4 — Matrix Parameters, Advanced Query Parameters, and Scrolling

Matrix Parameters (URL notation)

// Resulting URL: /products;categoryId=pies
<a [routerLink]="[PRODUCTS_ROUTE, { categoryId: selectedCategory }]">
  {{ category.name }}
</a>

// Reading in component (same API as route params)
@Input() categoryId?: string;  // via withComponentInputBinding

withInMemoryScrolling

provideRouter(routes,
  withInMemoryScrolling({
    scrollPositionRestoration: 'enabled',
    // 'disabled' → no scroll (default)
    // 'top'      → always scrolls to top
    // 'enabled'  → top when going forward, previous position when going back
    anchorScrolling: 'enabled', // enables fragment #anchor navigation
  })
)

5. Module 5 — Nested and Named RouterOutlets

Nested Routes

Zipper rule: there must be exactly 1 component declared for each RouterOutlet available on the activated route.

graph TD
    App["AppComponent\n<router-outlet> (primary)"]
    Products["ProductsWrapperComponent\n<router-outlet> (nested)"]
    AllProducts["AllProductsComponent"]
    Detail["ProductDetailComponent"]

    App -->|"/products"| Products
    Products -->|"/products (path: '')"| AllProducts
    Products -->|"/products/detail"| Detail
// Nested routes in app.routes.ts
{
  path: PRODUCTS_ROUTE,
  component: ProductsWrapperComponent,  // contains a nested <router-outlet>
  children: [
    {
      path: '',
      component: AllProductsComponent,
    },
    {
      path: DETAIL_ROUTE,
      component: ProductDetailComponent,
    },
  ],
},

Named Routes (Auxiliary / Secondary Routes)

<!-- app.html -->
<router-outlet />
<router-outlet name="cartModal" />
// Named route declaration
{
  path: CART_ROUTE,
  component: CartModalComponent,
  outlet: 'cartModal',
}

// Navigate to named outlet
<a [routerLink]="[{ outlets: { cartModal: [CART_ROUTE] } }]">
  Open Cart
</a>

// Close named outlet
this.router.navigate([{ outlets: { cartModal: null } }]);

6. Module 6 — Lazy Loading

What is Lazy Loading?

flowchart LR
    Init["Initial bundle\n(downloaded on startup)"]
    Chunk1["Chunk: products\n(downloaded on demand)"]
    Chunk2["Chunk: pizza\n(downloaded on demand)"]
    Chunk3["Chunk: login\n(downloaded on demand)"]

    Init -->|"Route /products activated"| Chunk1
    Init -->|"Route /pizza activated"| Chunk2
    Init -->|"Route /login activated"| Chunk3

loadComponent and loadChildren

export const routes: Routes = [
  // Eager (loaded in initial bundle)
  { path: HOME_ROUTE, component: HomeComponent },

  // Lazy Component
  {
    path: LOGIN_ROUTE,
    loadComponent: () =>
      import('./login/login.component').then(m => m.LoginComponent),
  },

  // Lazy Children
  {
    path: PRODUCTS_ROUTE,
    loadComponent: () =>
      import('./products/wrapper.component').then(m => m.WrapperComponent),
    loadChildren: () =>
      import('./products/products.routes').then(m => m.PRODUCTS_ROUTES),
  },

  {
    path: '**',
    loadComponent: () =>
      import('./not-found/not-found.component').then(m => m.NotFoundComponent),
  },
];

Route Preloading

import { PreloadAllModules, withPreloading } from '@angular/router';

provideRouter(routes,
  withPreloading(PreloadAllModules)
  // Starts downloading lazy chunks after initial navigation
)

7. Module 7 — Route Guards

Guards Overview

flowchart TD
    Nav["Navigation requested"]
    CM["CanMatch\n(filters route candidates\nbefore considering them)"]
    CA["CanActivate\n(controls route access)"]
    CAC["CanActivateChild\n(protects child routes)"]
    CD["CanDeactivate\n(prevents leaving a view)"]
    Allow["Navigation allowed"]
    Deny["Navigation cancelled / redirected"]

    Nav --> CM
    CM -->|"false → route ignored"| Nav
    CM -->|"true"| CA
    CA -->|"true"| CAC
    CA -->|"false / UrlTree"| Deny
    CAC -->|"true"| Allow
    CAC -->|"false"| Deny
    Allow -.->|"User navigates"| CD
    CD -->|"false"| Allow
    CD -->|"true"| Deny

CanActivate

// is-feature-enabled.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { map } from 'rxjs';

export const isFeatureEnabledGuard: CanActivateFn = (route, state) => {
  const router = inject(Router);
  const flagService = inject(FeatureFlagService);

  return flagService.isFeatureEnabled$.pipe(
    map(isEnabled =>
      isEnabled ? true : router.createUrlTree([HOME_ROUTE])
    )
  );
};

// Route declaration
{
  path: FEATURE_ROUTE,
  canActivate: [isFeatureEnabledGuard],
  loadComponent: () => import('./feature/feature.component').then(m => m.FeatureComponent),
}

CanMatch

// CanMatch runs before CanActivate — if false, route is skipped and next is tested
export const isFeatureEnabledCanMatchGuard: CanMatchFn = (route, segments) => {
  const flagService = inject(FeatureFlagService);
  return flagService.isFeatureEnabled$;
};

export const isUserAuthenticatedCanMatchGuard: CanMatchFn = (route, segments) => {
  const authService = inject(AuthService);
  return !!authService.authenticatedUser.value();
};

// Two routes on same path — CanMatch decides which one to use
{
  path: HOME_ROUTE,
  canMatch: [isFeatureEnabledCanMatchGuard],
  component: NewHomeComponent,
},
{
  path: HOME_ROUTE,
  component: HomeComponent,   // fallback if CanMatch returns false
},

CanDeactivate

// leave-form-can-deactivate.guard.ts
import { CanDeactivateFn } from '@angular/router';

export const leaveFormCanDeactivateGuard: CanDeactivateFn<FormComponent> = (
  component,
  currentRoute,
  currentState,
  nextState
) => {
  if (!component.hasUnsavedChanges()) return true;

  component.showLeaveModal.set(true);
  component.nextNavigation.set(nextState.url);
  return false;
};

// Declaration
{
  path: '',
  canDeactivate: [leaveFormCanDeactivateGuard],
  loadComponent: () => import('./form/form.component').then(m => m.FormComponent),
}

CLI Generation

ng generate guard is-feature-enabled
ng g guard leave-form-can-deactivate
ng generate resolver admin-user
Guard TypeRoute PropertyTypical Use Case
CanActivateFncanActivateAuthentication, feature flags
CanActivateChildFncanActivateChildProtect all child routes
CanMatchFncanMatchChoose between routes on same path
CanDeactivateFn<T>canDeactivateUnsaved forms

⚠️ Guards do not replace server-side security. Always validate permissions on the API side.


8. Module 8 — State via the Router (title, data, resolve, providers)

title — Page Title

// Static title
{ path: HOME_ROUTE, component: HomeComponent, title: "My App - Home" }

// Dynamic title via ResolveFn
{
  path: `${PRODUCTS_ROUTE}/:categoryId`,
  title: (route: ActivatedRouteSnapshot) => {
    const category = inject(CategoriesService).getCategoryById(
      route.paramMap.get('categoryId')!
    );
    return `My App - ${category.name}`;
  },
}

data — Static Data

{
  path: ABOUT_ROUTE,
  component: ImageWrapperComponent,
  data: {
    imageUrl: '/images/about-hero.jpg',
    routePath: ABOUT_ROUTE,
    component: AboutComponent,
  },
}

// Reading in component — via withComponentInputBinding, data is bound to @Input()
@Component({...})
export class ImageWrapperComponent {
  imageUrl = input<string>('');
  routePath = input<string>('');
  component = input<Type<unknown>>();
}

resolve — Dynamic Data

// admin-user.resolver.ts
export const adminUserResolver: ResolveFn<User | UrlTree> = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);

  if (!authService.authenticatedUser.hasValue()) {
    return new RedirectCommand(router.createUrlTree(['/', LOGIN_ROUTE]));
  }

  if (!authService.authenticatedUser.value()?.permissions.includes('admin')) {
    return new RedirectCommand(router.createUrlTree(['/', NOT_ADMIN_ROUTE]));
  }

  return authService.authenticatedUser.value();
};

// Route declaration
{
  path: ADMIN_ROUTE,
  loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent),
  resolve: { user: adminUserResolver },
  // 'user' key is bound to @Input() 'user' via ComponentInputBinding
}

⚠️ Resolvers block navigation until they resolve. Do not use them for large data volumes.

providers — Route-Level Dependencies

export const MESSAGE_SERVICE = new InjectionToken<MessageService>('MessageService');

// Route declaration — isolated instance for this route and its children
{
  path: PRODUCTS_ROUTE,
  providers: [
    { provide: MESSAGE_SERVICE, useValue: PRODUCTS_MESSAGE }
  ],
  loadChildren: () => import('./products/products.routes').then(m => m.PRODUCTS_ROUTES),
}

// Consumption
@Component({...})
export class ProductsComponent {
  private messageService = inject(MESSAGE_SERVICE);
  header = this.messageService.featureHeader;
}

9. Quick Reference

Routing Configuration Options

FeatureAPIDescription
Basic routesprovideRouter(routes)Required minimum
Component bindingwithComponentInputBinding()Binds params to @Input()
Scroll behaviorwithInMemoryScrolling(...)Controls scroll position
PreloadingwithPreloading(strategy)Preloads lazy chunks
View transitionswithViewTransitions(...)CSS transitions between views

Route Object Properties

PropertyTypePurpose
pathstringURL path segment
componentType<any>Eager component
loadComponent() => Promise<Type>Lazy component
loadChildren() => Promise<Routes>Lazy child routes
redirectTostringRedirect destination
pathMatch'full' | 'prefix'Path matching strategy
childrenRoutesChild routes
canActivateCanActivateFn[]Activation guards
canMatchCanMatchFn[]Match guards
canDeactivateCanDeactivateFn[]Deactivation guards
resolveResolveDataPre-navigation data fetching
dataDataStatic route data
titlestring | ResolveFnPage title
providersProvider[]Route-scoped DI providers
outletstringNamed outlet

CLI Commands

# Generate a guard
ng generate guard guard-name
ng g guard auth --functional

# Generate a resolver
ng generate resolver resolver-name
ng g r data

Best Practices

  • Export path constants (HOME_ROUTE, PRODUCTS_ROUTE) to avoid typos
  • Use loadComponent for everything not visible on the initial view
  • Prefer CanMatch over CanActivate when multiple routes share the same path
  • Guards never replace server-side security
  • Use withComponentInputBinding() to simplify route parameter reading

Search Terms

angular · routing · navigation · frontend · development · route · parameters · configuration · data · query · router · cli · guards · lazy · loading · matrix · named · nested · providers · resolve · routes · title

Interested in this course?

Contact us to book it or get a custom training plan for your team.