Angular Routing
What is Routing?
Routing enables navigation between different views/components in a single-page application (SPA) without full page reloads.
Basic Routing Setup
Step 1: Define Routes
// app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
import { ContactComponent } from './contact/contact.component';
export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'about', component: AboutComponent },
{ path: 'contact', component: ContactComponent },
{ path: '**', redirectTo: '' }, // Wildcard route
];
Step 2: Configure Application
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes)],
};
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
bootstrapApplication(AppComponent, appConfig);
Step 3: Add Router Outlet
// app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet, RouterLink } from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, RouterLink],
template: `
<nav>
<a routerLink="/">Home</a>
<a routerLink="/about">About</a>
<a routerLink="/contact">Contact</a>
</nav>
<router-outlet></router-outlet>
`,
})
export class AppComponent {}
RouterLink
Navigate declaratively in templates.
@Component({
template: `
<!-- Basic link -->
<a routerLink="/about">About</a>
<!-- Array syntax for complex paths -->
<a [routerLink]="['/users', userId]">User Profile</a>
<!-- Relative navigation -->
<a routerLink="../previous">Previous</a>
<a routerLink="./next">Next</a>
<!-- Query parameters -->
<a [routerLink]="['/search']" [queryParams]="{ q: 'angular' }">Search</a>
<!-- Fragment (hash) -->
<a routerLink="/docs" fragment="section1">Docs Section 1</a>
<!-- Preserve query params -->
<a routerLink="/next" queryParamsHandling="preserve">Next</a>
`,
})
export class NavigationComponent {
userId = 123;
}
RouterLinkActive
Highlight active links.
@Component({
template: `
<!-- Add 'active' class when route is active -->
<a routerLink="/" routerLinkActive="active">Home</a>
<!-- Multiple classes -->
<a routerLink="/about" routerLinkActive="active highlighted"> About </a>
<!-- Exact match (for root route) -->
<a
routerLink="/"
routerLinkActive="active"
[routerLinkActiveOptions]="{ exact: true }"
>
Home
</a>
<!-- Apply to parent element -->
<li routerLinkActive="active-item">
<a routerLink="/products">Products</a>
</li>
`,
styles: [
`
.active {
font-weight: bold;
color: blue;
}
`,
],
})
export class NavComponent {}
Programmatic Navigation
Navigate from component code.
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
@Component({
selector: 'app-login',
standalone: true,
template: ` <button (click)="login()">Login</button> `,
})
export class LoginComponent {
private router = inject(Router);
login() {
// Perform login...
// Navigate to dashboard
this.router.navigate(['/dashboard']);
// Navigate with extras
this.router.navigate(['/search'], {
queryParams: { q: 'angular', page: 1 },
fragment: 'results',
});
// Navigate by URL
this.router.navigateByUrl('/home');
// Relative navigation
this.router.navigate(['../sibling'], { relativeTo: this.route });
}
}
Route Parameters
Path Parameters
// Define route with parameter
const routes: Routes = [
{ path: 'users/:id', component: UserDetailComponent },
{ path: 'posts/:id/edit', component: PostEditComponent },
];
// Access parameters
import { Component, OnInit, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-user-detail',
standalone: true,
template: ` <h2>User ID: {{ userId }}</h2> `,
})
export class UserDetailComponent implements OnInit {
private route = inject(ActivatedRoute);
userId: string = '';
ngOnInit() {
// Snapshot (one-time read)
this.userId = this.route.snapshot.paramMap.get('id') || '';
// Observable (reactive updates)
this.route.paramMap.subscribe(params => {
this.userId = params.get('id') || '';
});
}
}
Query Parameters
// Navigate with query params
this.router.navigate(['/search'], {
queryParams: { q: 'angular', category: 'web' },
});
// URL: /search?q=angular&category=web
// Access query params
@Component({
selector: 'app-search',
standalone: true,
template: `<p>Search: {{ searchQuery }}</p>`,
})
export class SearchComponent implements OnInit {
private route = inject(ActivatedRoute);
searchQuery = '';
ngOnInit() {
// Snapshot
this.searchQuery = this.route.snapshot.queryParamMap.get('q') || '';
// Observable
this.route.queryParamMap.subscribe(params => {
this.searchQuery = params.get('q') || '';
});
}
}
Child Routes
Create nested routing structures.
const routes: Routes = [
{
path: 'dashboard',
component: DashboardComponent,
children: [
{ path: '', component: DashboardHomeComponent },
{ path: 'analytics', component: AnalyticsComponent },
{ path: 'reports', component: ReportsComponent },
],
},
];
// Parent component needs <router-outlet>
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [RouterOutlet, RouterLink],
template: `
<div class="dashboard">
<aside class="sidebar">
<a routerLink="">Home</a>
<a routerLink="analytics">Analytics</a>
<a routerLink="reports">Reports</a>
</aside>
<main class="content">
<router-outlet></router-outlet>
</main>
</div>
`,
})
export class DashboardComponent {}
Lazy Loading
Load routes on demand for better performance.
const routes: Routes = [
{
path: 'admin',
loadComponent: () =>
import('./admin/admin.component').then(m => m.AdminComponent),
},
{
path: 'products',
loadChildren: () =>
import('./products/products.routes').then(m => m.PRODUCT_ROUTES),
},
];
// products.routes.ts
import { Routes } from '@angular/router';
export const PRODUCT_ROUTES: Routes = [
{
path: '',
loadComponent: () =>
import('./product-list/product-list.component').then(
m => m.ProductListComponent
),
},
{
path: ':id',
loadComponent: () =>
import('./product-detail/product-detail.component').then(
m => m.ProductDetailComponent
),
},
];
Route Guards
Protect routes with guards.
Functional Guards (Modern Approach)
// auth.guard.ts
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from './auth.service';
export const authGuard = () => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isAuthenticated()) {
return true;
}
return router.createUrlTree(['/login']);
};
// Usage
const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
canActivate: [authGuard],
},
];
CanActivate - Control access to route
export const roleGuard = (requiredRole: string) => {
return () => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.hasRole(requiredRole)) {
return true;
}
return router.createUrlTree(['/unauthorized']);
};
};
// Usage
{
path: 'admin',
component: AdminComponent,
canActivate: [roleGuard('admin')]
}
CanDeactivate - Prevent leaving route
// unsaved-changes.guard.ts
export interface CanComponentDeactivate {
canDeactivate: () => boolean | Promise<boolean>;
}
export const unsavedChangesGuard = () => {
return (component: CanComponentDeactivate) => {
return component.canDeactivate
? component.canDeactivate()
: true;
};
};
// Component
@Component({
selector: 'app-edit-form',
standalone: true,
template: `<form>...</form>`
})
export class EditFormComponent implements CanComponentDeactivate {
hasUnsavedChanges = false;
canDeactivate(): boolean {
if (this.hasUnsavedChanges) {
return confirm('You have unsaved changes. Do you want to leave?');
}
return true;
}
}
// Route
{
path: 'edit/:id',
component: EditFormComponent,
canDeactivate: [unsavedChangesGuard()]
}
CanMatch - Control route matching
export const featureFlagGuard = (feature: string) => {
return () => {
const configService = inject(ConfigService);
return configService.isFeatureEnabled(feature);
};
};
// Usage - show route only if feature is enabled
{
path: 'beta-feature',
component: BetaComponent,
canMatch: [featureFlagGuard('beta')]
}
Resolvers
Pre-fetch data before activating route.
// user.resolver.ts
import { inject } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';
import { UserService } from './user.service';
export const userResolver = (route: ActivatedRouteSnapshot) => {
const userService = inject(UserService);
const userId = route.paramMap.get('id');
return userService.getUserById(userId!);
};
// Route
const routes: Routes = [
{
path: 'users/:id',
component: UserDetailComponent,
resolve: {
user: userResolver,
},
},
];
// Component
@Component({
selector: 'app-user-detail',
standalone: true,
template: `
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
`,
})
export class UserDetailComponent implements OnInit {
private route = inject(ActivatedRoute);
user: any;
ngOnInit() {
this.user = this.route.snapshot.data['user'];
// Or reactive
this.route.data.subscribe(data => {
this.user = data['user'];
});
}
}
Route Data
Pass static data to routes.
const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
data: {
title: 'Admin Dashboard',
breadcrumb: 'Admin',
requiresAuth: true,
},
},
];
// Access in component
@Component({
selector: 'app-admin',
standalone: true,
template: `<h1>{{ title }}</h1>`,
})
export class AdminComponent implements OnInit {
private route = inject(ActivatedRoute);
title = '';
ngOnInit() {
this.title = this.route.snapshot.data['title'];
}
}
Route Events
Listen to navigation events.
import { Component, OnInit, inject } from '@angular/core';
import {
Router,
NavigationStart,
NavigationEnd,
NavigationError,
} from '@angular/router';
import { filter } from 'rxjs/operators';
@Component({
selector: 'app-root',
standalone: true,
template: `
@if (isLoading) {
<div class="loading">Loading...</div>
}
<router-outlet></router-outlet>
`,
})
export class AppComponent implements OnInit {
private router = inject(Router);
isLoading = false;
ngOnInit() {
// Listen to all navigation events
this.router.events.subscribe(event => {
if (event instanceof NavigationStart) {
this.isLoading = true;
}
if (event instanceof NavigationEnd) {
this.isLoading = false;
console.log('Navigation completed:', event.url);
}
if (event instanceof NavigationError) {
this.isLoading = false;
console.error('Navigation error:', event.error);
}
});
// Filter specific events
this.router.events
.pipe(filter(event => event instanceof NavigationEnd))
.subscribe((event: any) => {
console.log('URL:', event.url);
});
}
}
Router State
Access current router state.
import { Component, inject } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-example',
standalone: true,
template: `<p>Current URL: {{ currentUrl }}</p>`,
})
export class ExampleComponent {
private router = inject(Router);
private route = inject(ActivatedRoute);
currentUrl = this.router.url;
getRouterInfo() {
// Current URL
console.log('URL:', this.router.url);
// Route params
console.log('Params:', this.route.snapshot.params);
// Query params
console.log('Query:', this.route.snapshot.queryParams);
// Fragment
console.log('Fragment:', this.route.snapshot.fragment);
// Router state
console.log('State:', this.router.routerState);
}
}
Best Practices
✅ Do
// Use exact match for root route
<a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">
// Use lazy loading for large features
{
path: 'admin',
loadComponent: () => import('./admin/admin.component')
}
// Use guards to protect routes
{
path: 'admin',
canActivate: [authGuard]
}
// Unsubscribe from route observables (if not using async pipe)
ngOnDestroy() {
this.subscription.unsubscribe();
}
// Use resolvers to pre-fetch data
{
path: 'user/:id',
resolve: { user: userResolver }
}
❌ Don't
// Don't forget wildcard route
// ❌ Missing
const routes: Routes = [
{ path: 'home', component: HomeComponent }
];
// ✅ With wildcard
const routes: Routes = [
{ path: 'home', component: HomeComponent },
{ path: '**', redirectTo: 'home' }
];
// Don't use href for internal navigation
// ❌ Bad
<a href="/about">About</a>
// ✅ Good
<a routerLink="/about">About</a>
// Don't forget to handle navigation errors
this.router.navigate(['/dashboard'])
.catch(error => console.error('Navigation failed:', error));
Interview Questions
Q: What's the difference between routerLink and href?
A: routerLink uses Angular's router for SPA navigation without page reload. href causes full page reload.
Q: What's the difference between CanActivate and CanMatch?
A: CanActivate decides if a route can be activated. CanMatch decides if a route should even be matched, useful for feature flags.
Q: When should you use lazy loading? A: Use lazy loading for large features or admin sections to reduce initial bundle size and improve load time.
Q: What's the difference between paramMap and queryParamMap?
A: paramMap contains route parameters from the URL path (/users/:id). queryParamMap contains query string parameters (/search?q=term).
Summary
✅ Basic routing with RouterLink and RouterOutlet ✅ Route parameters and query parameters ✅ Child routes and nested routing ✅ Lazy loading for performance ✅ Route guards for protection ✅ Resolvers for data pre-fetching ✅ Programmatic navigation ✅ Route events and state
Next Steps
- Complete the Fundamentals Project
- Move to Reactivity with Signals