Angular's router is one of the more powerful parts of the framework — it handles lazy loading, guards, nested routes, and query parameters. Here's everything you need for Angular 21 routing using the modern function-based approach.
Basic Route Setup
Define routes in app.routes.ts:
// src/app/app.routes.ts
import { Routes } from '@angular/router'
export const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'dashboard', loadComponent: () =>
import('./pages/dashboard/dashboard.component').then(m => m.DashboardComponent) },
{ path: 'templates', loadComponent: () =>
import('./pages/templates/templates.component').then(m => m.TemplatesComponent) },
{ path: 'templates/:id', loadComponent: () =>
import('./pages/template-detail/template-detail.component').then(m => m.TemplateDetailComponent) },
{ path: '**', loadComponent: () =>
import('./pages/not-found/not-found.component').then(m => m.NotFoundComponent) },
]
Register in main.ts:
// src/main.ts
import { bootstrapApplication } from '@angular/platform-browser'
import { provideRouter, withComponentInputBinding } from '@angular/router'
import { AppComponent } from './app/app.component'
import { routes } from './app/app.routes'
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes, withComponentInputBinding()) // enables route params as @Input
]
})
Add <router-outlet> in AppComponent:
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, RouterLink, RouterLinkActive],
template: `
<nav class="navbar navbar-expand-lg bg-dark navbar-dark px-3">
<a class="navbar-brand fw-bold" href="/">LettStart Admin</a>
<div class="navbar-nav ms-auto gap-2">
<a class="nav-link" routerLink="/dashboard" routerLinkActive="active">Dashboard</a>
<a class="nav-link" routerLink="/templates" routerLinkActive="active">Templates</a>
</div>
</nav>
<router-outlet />
`
})
export class AppComponent {}
Route Parameters
Two modern ways to read route params in Angular 21.
Option A — withComponentInputBinding() — cleanest approach:
// In main.ts: provideRouter(routes, withComponentInputBinding())
// In the component — route params become @Input() automatically
@Component({
selector: 'app-template-detail',
standalone: true,
template: `<h1>Template #{{ id }}</h1>`
})
export class TemplateDetailComponent {
@Input() id = '' // receives :id from route automatically
}
Option B — inject ActivatedRoute directly:
import { Component, inject, signal, OnInit } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
@Component({
selector: 'app-template-detail',
standalone: true,
template: `
<div class="container py-4">
@if (template()) {
<h1>{{ template()!.name }}</h1>
<p class="text-muted">{{ template()!.description }}</p>
<span class="badge text-white" style="background:#fd4766;">
{{ template()!.framework }}
</span>
}
</div>
`
})
export class TemplateDetailComponent implements OnInit {
private route = inject(ActivatedRoute)
template = signal<any>(null)
ngOnInit() {
const id = this.route.snapshot.paramMap.get('id')
// fetch template by id...
this.template.set({ name: 'Marvel Dashboard', description: 'Angular 21 + Bootstrap 5', framework: 'Angular' })
}
}
Query Parameters
import { Router, ActivatedRoute } from '@angular/router'
export class TemplatesComponent {
private router = inject(Router)
private route = inject(ActivatedRoute)
// Navigate with query params
filterByFramework(framework: string) {
this.router.navigate(['/templates'], {
queryParams: { framework, page: 1 }
})
}
// Read query params
ngOnInit() {
this.route.queryParams.subscribe(params => {
console.log(params['framework']) // 'Angular'
console.log(params['page']) // '1'
})
}
}
Lazy Loading
Every route in the example already uses loadComponent — this is lazy loading by default in Angular 21. Angular bundles each lazily-loaded component separately and only downloads the code when the user navigates there.
For a group of related routes, use loadChildren:
export const routes: Routes = [
{
path: 'admin',
loadChildren: () => import('./admin/admin.routes').then(m => m.adminRoutes)
}
]
// src/app/admin/admin.routes.ts
import { Routes } from '@angular/router'
export const adminRoutes: Routes = [
{ path: '', loadComponent: () => import('./admin-dashboard.component').then(m => m.AdminDashboardComponent) },
{ path: 'users', loadComponent: () => import('./users.component').then(m => m.UsersComponent) },
{ path: 'settings', loadComponent: () => import('./settings.component').then(m => m.SettingsComponent) },
]
Route Guards
Modern Angular uses plain functions for guards, not injectable classes:
// src/app/guards/auth.guard.ts
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, preserving the intended URL
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url }
})
}
// Apply to routes
export const routes: Routes = [
{
path: 'dashboard',
canActivate: [authGuard],
loadComponent: () => import('./dashboard.component').then(m => m.DashboardComponent)
}
]
canDeactivate guard — warn before leaving a form with unsaved changes:
import { CanDeactivateFn } from '@angular/router'
export interface HasUnsavedChanges {
hasUnsavedChanges(): boolean
}
export const unsavedChangesGuard: CanDeactivateFn<HasUnsavedChanges> = (component) => {
if (component.hasUnsavedChanges()) {
return confirm('You have unsaved changes. Leave anyway?')
}
return true
}
Nested / Child Routes
export const routes: Routes = [
{
path: 'account',
loadComponent: () => import('./account/account.component').then(m => m.AccountComponent),
children: [
{ path: '', redirectTo: 'profile', pathMatch: 'full' },
{ path: 'profile', loadComponent: () => import('./account/profile.component').then(m => m.ProfileComponent) },
{ path: 'billing', loadComponent: () => import('./account/billing.component').then(m => m.BillingComponent) },
{ path: 'security', loadComponent: () => import('./account/security.component').then(m => m.SecurityComponent) },
]
}
]
// AccountComponent — needs its own router-outlet for child routes
@Component({
selector: 'app-account',
standalone: true,
imports: [RouterOutlet, RouterLink, RouterLinkActive],
template: `
<div class="container py-4">
<div class="row g-4">
<div class="col-md-3">
<div class="list-group">
<a class="list-group-item list-group-item-action"
routerLink="profile" routerLinkActive="active">Profile</a>
<a class="list-group-item list-group-item-action"
routerLink="billing" routerLinkActive="active">Billing</a>
<a class="list-group-item list-group-item-action"
routerLink="security" routerLinkActive="active">Security</a>
</div>
</div>
<div class="col-md-9">
<router-outlet />
</div>
</div>
</div>
`
})
export class AccountComponent {}
Programmatic Navigation
export class NavService {
private router = inject(Router)
goToDashboard() {
this.router.navigate(['/dashboard'])
}
goToTemplate(id: string) {
this.router.navigate(['/templates', id])
}
goToTemplatesFiltered(framework: string) {
this.router.navigate(['/templates'], {
queryParams: { framework },
queryParamsHandling: 'merge' // keeps existing query params
})
}
goBack() {
window.history.back()
}
}
Frequently Asked Questions
Need a Full Bootstrap 5 Admin Dashboard?
Get a complete Angular 21 + Bootstrap 5 dashboard with 50+ components — built by the same team behind BootstrapPlanet.
Browse Templates →Use code FIRST30 for 30% off your first purchase.