Chapter 8: Routing and Navigation
Most useful applications aren’t made up of a single screen. They involve multiple views: a dashboard, a list of users, a detail page, a settings panel. To move between these, we need a routing system.
Angular provides a powerful, built-in Router that allows you to:
- Define application routes.
- Navigate between views.
- Pass parameters.
- Protect certain routes with guards.
- Optimize loading with lazy modules or standalone APIs.
In this chapter, we’ll build a strong understanding of Angular’s routing system, from the basics to advanced features.
8.1 Setting Up Routing
When generating a new Angular project, the CLI asks:
? Would you like to add Angular routing? (y/N)
If you answer Yes, it configures RouterModule automatically. If not, you can add it later:
import { provideRouter, Routes } from '@angular/router';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
const routes: Routes = [];
bootstrapApplication(AppComponent, {
providers: [provideRouter(routes)]
});
Here we’re using the standalone API (provideRouter), which is the modern way to set up routing.
8.2 Defining Routes
A route maps a URL path to a component.
import { Routes } from '@angular/router';
import { HomeComponent } from './home.component';
import { AboutComponent } from './about.component';
export const routes: Routes = [
{ path: '', component: HomeComponent }, // default route
{ path: 'about', component: AboutComponent }
];
In your root template (app.component.html), add the <router-outlet> directive:
<nav>
<a routerLink="">Home</a>
<a routerLink="about">About</a>
</nav>
<router-outlet></router-outlet>
Now your app has two navigable views.
8.3 Navigating Between Routes
There are three ways to navigate:
- Declarative links (preferred for most cases):
<a routerLink="/about">About</a>
- Programmatic navigation using
Router:
import { Router } from '@angular/router';
@Component({ /* ... */ })
export class HomeComponent {
constructor(private router: Router) {}
goToAbout() {
this.router.navigate(['/about']);
}
}
- Relative navigation (relative to current route):
this.router.navigate(['../profile'], { relativeTo: this.route });
8.4 Route Parameters
Sometimes we need dynamic routes, like /users/42.
{ path: 'users/:id', component: UserDetailComponent }
In UserDetailComponent, you can access the parameter:
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-user-detail',
standalone: true,
template: `<h2>User ID: {{ userId }}</h2>`
})
export class UserDetailComponent {
userId!: string;
constructor(private route: ActivatedRoute) {
this.userId = this.route.snapshot.paramMap.get('id')!;
}
}
For reactive updates (when navigating between users while staying on the same page):
this.route.paramMap.subscribe(params => {
this.userId = params.get('id')!;
});
8.5 Query Parameters and Fragments
Add optional data to a URL with query parameters:
<a [routerLink]="['/search']" [queryParams]="{ q: 'angular' }">
Search Angular
</a>
In the component:
this.route.queryParamMap.subscribe(params => {
console.log(params.get('q')); // "angular"
});
Fragments (#anchor) are also supported via this.route.fragment.
8.6 Nested Routes (Child Routes)
You can define child routes inside a parent.
{
path: 'admin',
component: AdminComponent,
children: [
{ path: 'users', component: AdminUsersComponent },
{ path: 'settings', component: AdminSettingsComponent }
]
}
<router-outlet> inside AdminComponent renders the child views.
8.7 Lazy Loading
For large apps, it’s inefficient to load everything up front. Lazy loading lets Angular load features only when needed.
With standalone APIs:
{
path: 'products',
loadComponent: () =>
import('./products/products.component').then(m => m.ProductsComponent)
}
This way, the Products feature is only downloaded when the user navigates to /products.
8.8 Route Guards
Some routes should only be accessible under certain conditions — e.g., if the user is logged in. Guards let you protect routes.
Example AuthGuard:
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService);
const router = inject(Router);
return auth.isLoggedIn() ? true : router.parseUrl('/login');
};
Use it in routes:
{ path: 'dashboard', component: DashboardComponent, canActivate: [authGuard] }
8.9 Route Resolvers
Resolvers let you fetch data before navigation completes, ensuring the component has the data it needs immediately.
import { ResolveFn } from '@angular/router';
import { UserService } from './user.service';
export const userResolver: ResolveFn<any> = (route) => {
const service = inject(UserService);
return service.getUser(route.paramMap.get('id')!);
};
Route definition:
{ path: 'users/:id', component: UserDetailComponent, resolve: { user: userResolver } }
Inside the component:
this.route.data.subscribe(data => {
this.user = data['user'];
});
8.10 Best Practices
- Keep routes organized by feature.
- Prefer standalone route definitions (
provideRouter). - Use lazy loading for large features.
- Use guards for security, not just UI (still validate on the backend).
- Avoid deeply nested routes unless truly necessary.
8.11 Summary
- Angular’s router lets you define paths, navigate, and manage dynamic data in URLs.
- Routing works with standalone APIs, eliminating the need for NgModules.
- Features include parameters, query params, nested routes, lazy loading, guards, and resolvers.