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

Define a routes array in app.routes.ts, then pass it to provideRouter(routes) in your bootstrapApplication() call in main.ts. Add <router-outlet> to your root AppComponent template where routed components should render.
Inject the Router service and call this.router.navigate(['/path']) for absolute navigation, or this.router.navigate(['../sibling'], { relativeTo: this.route }) for relative navigation. Pass query params as the second argument: { queryParams: { page: 1 } }.
Set loadComponent in a route to a dynamic import: { path: 'dashboard', loadComponent: () => import('./dashboard.component').then(m => m.DashboardComponent) }. Angular only downloads that component's code when the user navigates to that route.
A route guard is a function that runs before a route activates and returns true (allow) or false/UrlTree (block/redirect). The canActivate guard controls whether a route can be entered. The canDeactivate guard asks if a user can leave a route — useful for unsaved form warning.

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.

Related Components