Angular: Zero to Senior · Lesson 5 of 11

Routing & Navigation

The Angular Router maps URLs to components. It supports nested routes, lazy loading, route guards, and history-based navigation — everything a real SPA needs.


Basic Setup

When you run ng new my-app --routing, Angular generates app.routes.ts and wires it up in app.config.ts.

src/app/app.routes.ts

TYPESCRIPT
import { Routes } from '@angular/router';
import { HomeComponent }    from './home/home.component';
import { AboutComponent }   from './about/about.component';
import { NotFoundComponent } from './not-found/not-found.component';

export const routes: Routes = [
  { path: '',        component: HomeComponent },
  { path: 'about',  component: AboutComponent },
  { path: '**',     component: NotFoundComponent },   // catch-all — must be last
];

src/app/app.config.ts

TYPESCRIPT
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes)],
};

app.component.html — add the router outlet:

HTML
<nav>
  <a routerLink="/">Home</a>
  <a routerLink="/about">About</a>
</nav>

<router-outlet />

Import RouterOutlet and RouterLink in your component:

TYPESCRIPT
@Component({
  standalone: true,
  imports: [RouterOutlet, RouterLink],
  // ...
})
export class AppComponent {}

RouterLink & RouterLinkActive

HTML
<!-- Static link -->
<a routerLink="/users">Users</a>

<!-- Dynamic link  uses an array -->
<a [routerLink]="['/users', user.id]">{{ user.name }}</a>

<!-- Link with query params -->
<a [routerLink]="['/users']" [queryParams]="{ page: 2, sort: 'name' }">Page 2</a>

<!-- Active class  applies 'active' when route matches -->
<a routerLink="/dashboard" routerLinkActive="active">Dashboard</a>

<!-- Exact match (root path needs this) -->
<a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Home</a>

Route Parameters

Route Params — /users/:id

TYPESCRIPT
// Route definition
{ path: 'users/:id', component: UserDetailComponent }

Reading the param:

TYPESCRIPT
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { inject } from '@angular/core';

@Component({ standalone: true, template: `<p>User ID: {{ userId }}</p>` })
export class UserDetailComponent implements OnInit {
  private route = inject(ActivatedRoute);
  userId!: number;

  ngOnInit(): void {
    this.userId = Number(this.route.snapshot.paramMap.get('id'));
  }
}

For params that change without navigation (same component, different ID):

TYPESCRIPT
ngOnInit(): void {
  this.route.paramMap.subscribe(params => {
    this.userId = Number(params.get('id'));
    this.loadUser(this.userId);
  });
}

Query Params — /users?page=2&sort=name

TYPESCRIPT
ngOnInit(): void {
  this.route.queryParamMap.subscribe(params => {
    this.page = Number(params.get('page') ?? 1);
    this.sort = params.get('sort') ?? 'id';
  });
}

Route Data — Static Metadata

TYPESCRIPT
{ path: 'admin', component: AdminComponent, data: { title: 'Admin Panel', role: 'admin' } }
TYPESCRIPT
ngOnInit(): void {
  const title = this.route.snapshot.data['title'];
}

Programmatic Navigation

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

@Component({ /* ... */ })
export class LoginComponent {
  private router = inject(Router);

  onLogin(): void {
    // after successful login...
    this.router.navigate(['/dashboard']);

    // with params
    this.router.navigate(['/users', userId]);

    // with query params
    this.router.navigate(['/products'], { queryParams: { category: 'electronics' } });

    // relative navigation
    this.router.navigate(['../sibling'], { relativeTo: this.route });
  }
}

Nested Routes (Children)

TYPESCRIPT
export const routes: Routes = [
  {
    path: 'admin',
    component: AdminLayoutComponent,   // layout component with <router-outlet>
    children: [
      { path: '',        redirectTo: 'dashboard', pathMatch: 'full' },
      { path: 'dashboard', component: AdminDashboardComponent },
      { path: 'users',     component: AdminUsersComponent },
      { path: 'settings',  component: AdminSettingsComponent },
    ],
  },
];

AdminLayoutComponent must have its own <router-outlet> for children to render into.


Lazy Loading (Essential for Performance)

Don't bundle all feature code upfront. Lazy load routes so code is only downloaded when needed:

TYPESCRIPT
export const routes: Routes = [
  { path: '',        component: HomeComponent },
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.routes').then(m => m.adminRoutes),
  },
  {
    path: 'shop',
    loadChildren: () => import('./shop/shop.routes').then(m => m.shopRoutes),
  },
];

src/app/admin/admin.routes.ts

TYPESCRIPT
import { Routes } from '@angular/router';

export const adminRoutes: Routes = [
  { path: '',          component: AdminDashboardComponent },
  { path: 'users',     component: AdminUsersComponent },
  { path: 'settings',  component: AdminSettingsComponent },
];

Angular creates a separate JS bundle for each lazy route, significantly improving initial load time.

You can also lazy load a single component:

TYPESCRIPT
{
  path: 'heavy-page',
  loadComponent: () => import('./heavy/heavy.component').then(m => m.HeavyComponent),
}

Route Guards

Guards control who can access a route.

CanActivate Guard

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

export const authGuard: CanActivateFn = (route, state) => {
  const auth   = inject(AuthService);
  const router = inject(Router);

  if (auth.isLoggedIn()) {
    return true;
  }

  // Redirect to login, preserve the attempted URL
  router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
  return false;
};

Apply to routes:

TYPESCRIPT
{ path: 'dashboard', component: DashboardComponent, canActivate: [authGuard] },
{ path: 'admin',     loadChildren: () => import('./admin/admin.routes').then(m => m.adminRoutes), canActivate: [authGuard, adminGuard] },

CanDeactivate Guard (Unsaved Changes)

TYPESCRIPT
import { CanDeactivateFn } from '@angular/router';

export interface CanDeactivateComponent {
  hasUnsavedChanges(): boolean;
}

export const unsavedChangesGuard: CanDeactivateFn<CanDeactivateComponent> = (component) => {
  if (component.hasUnsavedChanges()) {
    return confirm('You have unsaved changes. Leave anyway?');
  }
  return true;
};
TYPESCRIPT
@Component({ /* ... */ })
export class EditProfileComponent implements CanDeactivateComponent {
  isDirty = false;

  hasUnsavedChanges(): boolean {
    return this.isDirty;
  }
}

Resolve Guard (Pre-fetch Data)

TYPESCRIPT
import { ResolveFn } from '@angular/router';
import { inject } from '@angular/core';
import { UserService } from './services/user.service';

export const userResolver: ResolveFn<User> = (route) => {
  return inject(UserService).getById(Number(route.paramMap.get('id')));
};
TYPESCRIPT
{ path: 'users/:id', component: UserDetailComponent, resolve: { user: userResolver } }
TYPESCRIPT
ngOnInit(): void {
  const user = this.route.snapshot.data['user'] as User;
}

Router Events

Listen to navigation events for loading indicators, analytics, etc.:

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

@Component({ /* ... */ })
export class AppComponent {
  isLoading = false;
  private router = inject(Router);

  constructor() {
    this.router.events.subscribe(event => {
      if (event instanceof NavigationStart) this.isLoading = true;
      if (event instanceof NavigationEnd)   this.isLoading = false;
    });
  }
}

Quick Reference

Setup:              provideRouter(routes) in app.config.ts
Template link:      routerLink="/path" or [routerLink]="['/path', id]"
Active class:       routerLinkActive="active"
Route params:       route.snapshot.paramMap.get('id')
Query params:       route.snapshot.queryParamMap.get('page')
Navigate:           router.navigate(['/path', id], { queryParams: {} })
Nested routes:      children: [] in route,  in parent
Lazy routes:        loadChildren: () => import(...).then(m => m.routes)
Lazy component:     loadComponent: () => import(...).then(m => m.Component)
Guards:             canActivate: [myGuard]  (CanActivateFn)
Pre-fetch:          resolve: { key: myResolver }  (ResolveFn)