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
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
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes)],
};app.component.html — add the router outlet:
<nav>
<a routerLink="/">Home</a>
<a routerLink="/about">About</a>
</nav>
<router-outlet />Import RouterOutlet and RouterLink in your component:
@Component({
standalone: true,
imports: [RouterOutlet, RouterLink],
// ...
})
export class AppComponent {}RouterLink & RouterLinkActive
<!-- 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
// Route definition
{ path: 'users/:id', component: UserDetailComponent }Reading the param:
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):
ngOnInit(): void {
this.route.paramMap.subscribe(params => {
this.userId = Number(params.get('id'));
this.loadUser(this.userId);
});
}Query Params — /users?page=2&sort=name
ngOnInit(): void {
this.route.queryParamMap.subscribe(params => {
this.page = Number(params.get('page') ?? 1);
this.sort = params.get('sort') ?? 'id';
});
}Route Data — Static Metadata
{ path: 'admin', component: AdminComponent, data: { title: 'Admin Panel', role: 'admin' } }ngOnInit(): void {
const title = this.route.snapshot.data['title'];
}Programmatic Navigation
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)
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:
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
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:
{
path: 'heavy-page',
loadComponent: () => import('./heavy/heavy.component').then(m => m.HeavyComponent),
}Route Guards
Guards control who can access a route.
CanActivate Guard
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:
{ path: 'dashboard', component: DashboardComponent, canActivate: [authGuard] },
{ path: 'admin', loadChildren: () => import('./admin/admin.routes').then(m => m.adminRoutes), canActivate: [authGuard, adminGuard] },CanDeactivate Guard (Unsaved Changes)
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;
};@Component({ /* ... */ })
export class EditProfileComponent implements CanDeactivateComponent {
isDirty = false;
hasUnsavedChanges(): boolean {
return this.isDirty;
}
}Resolve Guard (Pre-fetch Data)
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')));
};{ path: 'users/:id', component: UserDetailComponent, resolve: { user: userResolver } }ngOnInit(): void {
const user = this.route.snapshot.data['user'] as User;
}Router Events
Listen to navigation events for loading indicators, analytics, etc.:
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)