Angular Routing & Navigation
Set up the Angular Router, define routes, navigate programmatically, read route params and query strings, protect routes with guards, and lazy load feature modules.
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) Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.