Building a single-page application sounds simple until you need to handle routing. I've seen Angular applications where every route was eagerly loaded (making the initial bundle huge), routes weren't protected (allowing unauthorized access), and navigation was handled inconsistently. When I learned how to properly configure Angular Router, it transformed how I build applications.
Angular Router is a powerful routing framework that enables navigation between views based on URL changes. It supports lazy loading (loading modules only when needed), route guards (protecting routes based on authentication or authorization), route parameters (dynamic segments like /products/:id), query parameters (for filtering and searching), and nested routes (for complex layouts). Understanding these features is crucial for building scalable Angular applications.
π Want the complete guide with more examples and advanced patterns? Check out the full article on my blog for an in-depth tutorial with additional code examples, troubleshooting tips, and real-world use cases.
What is Angular Router?
Angular Router provides:
- Client-side routing - Navigation without page refreshes
- Lazy loading - Load modules on-demand for better performance
- Route guards - Protect routes with authentication/authorization
- Route parameters - Dynamic route segments
- Query parameters - Optional filtering and search
- Nested routes - Complex layouts with child routes
- Route resolvers - Prefetch data before route activation
Basic Route Configuration
Set up routing in your app routing module:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DashboardComponent } from './dashboard/dashboard.component';
import { BusinessListComponent } from './business/business-list.component';
import { UserListComponent } from './users/user-list.component';
const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'business', component: BusinessListComponent },
{ path: 'users', component: UserListComponent },
{ path: '**', redirectTo: '/dashboard' } // Wildcard route (404)
];
@NgModule({
imports: [RouterModule.forRoot(routes, {
enableTracing: false, // Set to true for debugging
useHash: false // Use hash-based routing if true
})],
exports: [RouterModule]
})
export class AppRoutingModule { }
Router Outlet
Add <router-outlet> in your template to display routed components:
<!-- app.component.html -->
<nav>
<a routerLink="/dashboard">Dashboard</a>
<a routerLink="/business">Business</a>
<a routerLink="/users">Users</a>
</nav>
<router-outlet></router-outlet>
Lazy Loading Modules
Implement lazy loading for better performance:
const routes: Routes = [
{
path: 'business',
loadChildren: () => import('./business/business.module')
.then(m => m.BusinessModule)
},
{
path: 'users',
loadChildren: () => import('./users/users.module')
.then(m => m.UsersModule)
},
{
path: 'sites',
loadChildren: () => import('./site/site.module')
.then(m => m.SiteModule)
}
];
Feature Module Routing
Each lazy-loaded module has its own routing:
// business-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { BusinessListComponent } from './business-list.component';
import { BusinessDetailsComponent } from './business-details.component';
const businessRoutes: Routes = [
{ path: '', component: BusinessListComponent },
{ path: ':id', component: BusinessDetailsComponent },
{ path: ':id/settings', component: BusinessSettingsComponent }
];
@NgModule({
imports: [RouterModule.forChild(businessRoutes)], // Use forChild, not forRoot
exports: [RouterModule]
})
export class BusinessRoutingModule { }
Key Points:
- Use
loadChildrenwith dynamic import syntax - Use
RouterModule.forChild()in feature modules - Routes are relative to the feature module path
- Modules load only when their routes are accessed
Route Guards
Protect routes with guards:
import { Injectable } from '@angular/core';
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { AuthService } from '../auth/auth.service';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router
) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> | Promise<boolean> | boolean {
return this.authService.isAuthenticated.pipe(
map(isAuthenticated => {
if (!isAuthenticated) {
localStorage.setItem('returnUrl', state.url);
this.authService.Logout();
this.router.navigate(['/login']);
return false;
}
return true;
})
);
}
}
// Use in routes
const routes: Routes = [
{
path: 'business',
canActivate: [AuthGuard],
loadChildren: () => import('./business/business.module').then(m => m.BusinessModule)
}
];
Multiple Guards
const routes: Routes = [
{
path: 'admin',
canActivate: [AuthGuard, RoleGuard],
data: { role: 'admin' },
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
}
];
Route Parameters
Access route parameters in components:
// Route definition
{ path: 'business/:id', component: BusinessDetailsComponent }
{ path: 'business/:id/edit/:section', component: BusinessEditComponent }
// Component
import { ActivatedRoute, Router } from '@angular/router';
export class BusinessDetailsComponent implements OnInit {
businessId: number;
constructor(
private route: ActivatedRoute,
private router: Router
) {}
ngOnInit(): void {
// Snapshot (one-time, use when component won't be reused)
this.businessId = +this.route.snapshot.params['id'];
// Observable (for dynamic updates when route params change)
this.route.params.subscribe(params => {
this.businessId = +params['id'];
this.loadBusiness();
});
// Using paramMap (recommended)
this.route.paramMap.subscribe(params => {
this.businessId = +params.get('id')!;
this.loadBusiness();
});
}
navigateToEdit(): void {
this.router.navigate(['/business', this.businessId, 'edit']);
}
}
Multiple Route Parameters
// Route
{ path: 'business/:businessId/site/:siteId', component: SiteDetailsComponent }
// Component
this.route.params.subscribe(params => {
const businessId = +params['businessId'];
const siteId = +params['siteId'];
this.loadSite(businessId, siteId);
});
Query Parameters
Handle query parameters for filtering and searching:
// Navigate with query params
this.router.navigate(['/business'], {
queryParams: { page: 1, size: 10, search: 'term' }
});
// Navigate with query params and fragment
this.router.navigate(['/business'], {
queryParams: { filter: 'active' },
fragment: 'section-1'
});
// Read query params
export class BusinessListComponent implements OnInit {
constructor(private route: ActivatedRoute) {}
ngOnInit(): void {
// Snapshot (one-time)
const page = this.route.snapshot.queryParams['page'] || 1;
const size = this.route.snapshot.queryParams['size'] || 10;
// Observable (for dynamic updates)
this.route.queryParams.subscribe(params => {
const page = params['page'] || 1;
const size = params['size'] || 10;
const search = params['search'] || '';
this.loadData(page, size, search);
});
// Using queryParamMap (recommended)
this.route.queryParamMap.subscribe(params => {
const page = +params.get('page') || 1;
const size = +params.get('size') || 10;
this.loadData(page, size);
});
}
}
// Preserve query params on navigation
this.router.navigate(['/users'], {
queryParamsHandling: 'preserve'
});
// Merge query params
this.router.navigate(['/business'], {
queryParams: { page: 2 },
queryParamsHandling: 'merge'
});
Template Query Parameters
<!-- Template navigation with query params -->
<a [routerLink]="['/business']" [queryParams]="{page: 1, size: 10}">
Business List
</a>
<a [routerLink]="['/search']"
[queryParams]="{q: searchTerm}"
queryParamsHandling="merge">
Search
</a>
Navigation Methods
Different ways to navigate in Angular:
import { Router, ActivatedRoute } from '@angular/router';
export class MyComponent {
constructor(
private router: Router,
private route: ActivatedRoute
) {}
// Basic navigation
navigateToBusiness(): void {
this.router.navigate(['/business']);
}
// Navigation with route parameters
navigateWithParams(): void {
this.router.navigate(['/business', 123]);
}
// Navigation with query parameters
navigateWithQueryParams(): void {
this.router.navigate(['/business'], {
queryParams: { page: 1, filter: 'active' }
});
}
// Relative navigation
navigateRelative(): void {
this.router.navigate(['../users'], { relativeTo: this.route });
}
// Navigation with options
navigateWithOptions(): void {
this.router.navigate(['/business'], {
queryParams: { page: 1 },
fragment: 'top',
queryParamsHandling: 'merge',
preserveFragment: true
});
}
// Navigate by URL (absolute path)
navigateByUrl(): void {
this.router.navigateByUrl('/business/123?page=1');
}
// Navigate with state
navigateWithState(): void {
this.router.navigate(['/business'], {
state: { fromDashboard: true }
});
}
}
Template Navigation
<!-- Basic routerLink -->
<a routerLink="/business">Business</a>
<!-- RouterLink with parameters -->
<a [routerLink]="['/business', businessId]">Business Details</a>
<!-- RouterLink with query params -->
<a [routerLink]="['/business']" [queryParams]="{page: 1}">
Business List
</a>
<!-- Active link styling -->
<a routerLink="/business" routerLinkActive="active">
Business
</a>
<!-- Active link with exact match -->
<a routerLink="/business"
routerLinkActive="active"
[routerLinkActiveOptions]="{exact: true}">
Business
</a>
Nested Routes
Create nested routes for complex layouts:
// Parent route
const routes: Routes = [
{
path: 'business',
component: BusinessLayoutComponent,
children: [
{ path: '', component: BusinessListComponent },
{ path: ':id', component: BusinessDetailsComponent },
{ path: ':id/settings', component: BusinessSettingsComponent }
]
}
];
// BusinessLayoutComponent template
<div class="business-layout">
<nav>
<a routerLink="/business" routerLinkActive="active">List</a>
<a routerLink="/business/123" routerLinkActive="active">Details</a>
</nav>
<router-outlet></router-outlet> <!-- Child routes render here -->
</div>
Route Data
Pass static data to routes:
const routes: Routes = [
{
path: 'business',
component: BusinessComponent,
data: {
title: 'Business Management',
requiresAuth: true,
roles: ['admin', 'manager']
}
}
];
// Access in component
export class BusinessComponent implements OnInit {
constructor(private route: ActivatedRoute) {}
ngOnInit(): void {
const title = this.route.snapshot.data['title'];
const requiresAuth = this.route.snapshot.data['requiresAuth'];
// Or subscribe to data changes
this.route.data.subscribe(data => {
console.log(data.title);
});
}
}
Route Resolvers
Prefetch data before route activation:
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { BusinessService } from './business.service';
@Injectable({
providedIn: 'root'
})
export class BusinessResolver implements Resolve<any> {
constructor(private businessService: BusinessService) {}
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<any> | Promise<any> | any {
const businessId = +route.params['id'];
return this.businessService.GetBusiness(businessId);
}
}
// Use in routes
const routes: Routes = [
{
path: 'business/:id',
component: BusinessDetailsComponent,
resolve: { business: BusinessResolver }
}
];
// Access in component
export class BusinessDetailsComponent implements OnInit {
business: Business;
constructor(private route: ActivatedRouteSnapshot) {}
ngOnInit(): void {
// Data is already loaded by resolver
this.business = this.route.snapshot.data['business'];
}
}
Advanced Patterns
Route Events
Listen to route changes:
import { Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs/operators';
export class AppComponent implements OnInit {
constructor(private router: Router) {}
ngOnInit(): void {
this.router.events
.pipe(filter(event => event instanceof NavigationEnd))
.subscribe((event: NavigationEnd) => {
console.log('Navigation ended:', event.url);
// Track analytics, update breadcrumbs, etc.
});
}
}
Redirect After Login
export class LoginComponent {
constructor(
private router: Router,
private route: ActivatedRoute
) {}
onLoginSuccess(): void {
const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/dashboard';
this.router.navigate([returnUrl]);
}
}
// In guard
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
if (!this.authService.isAuthenticated) {
this.router.navigate(['/login'], {
queryParams: { returnUrl: state.url }
});
return false;
}
return true;
}
Best Practices
- Use lazy loading for feature modules - Reduce initial bundle size
- Implement route guards - For authentication and authorization
- Use route data - Pass static data to components
- Handle route parameters with observables - For dynamic updates
- Use query parameters - For optional, filterable data
- Implement proper error handling - For route navigation
- Use RouterLink directive - In templates for declarative navigation
- Store return URLs - For redirecting after authentication
- Use route resolvers - To prefetch data before route activation
- Organize routes properly - Use feature routing modules
- Handle 404 routes - With wildcard routes
- Use route data for permissions - Pass role/permission info
Common Patterns
Route Configuration Structure
// app-routing.module.ts - Main routes
const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
{
path: 'business',
canActivate: [AuthGuard],
loadChildren: () => import('./business/business.module').then(m => m.BusinessModule)
},
{ path: '**', component: NotFoundComponent }
];
Active Route Styling
<nav>
<a routerLink="/dashboard"
routerLinkActive="active"
[routerLinkActiveOptions]="{exact: true}">
Dashboard
</a>
<a routerLink="/business" routerLinkActive="active">
Business
</a>
</nav>
Resources and Further Reading
- π Full Angular Routing Guide - Complete tutorial with advanced examples, troubleshooting, and best practices
- Angular Guards Guide - Route protection patterns
- Angular Modules Guide - Module architecture with lazy loading
- Angular HTTP Client Guide - HTTP requests with routing
- Angular Router Documentation - Official Angular docs
- Angular Lazy Loading Guide - Official lazy loading guide
- Angular Route Guards - Route guards documentation
Conclusion
Angular Router provides a comprehensive routing solution for building single-page applications. With lazy loading, route guards, and navigation patterns, you can create scalable, maintainable routing architectures for enterprise Angular applications.
Key Takeaways:
- Angular Router - Client-side routing framework
- Lazy Loading - Load modules on-demand for better performance
- Route Guards - Protect routes with authentication/authorization
- Route Parameters - Dynamic route segments
- Query Parameters - Optional filtering and search
- Nested Routes - Complex layouts with child routes
- Route Resolvers - Prefetch data before route activation
- Navigation Methods - RouterLink and programmatic navigation
Whether you're building a simple dashboard or a complex enterprise application, Angular Router provides the foundation you need. It handles all the routing logic while giving you complete control over navigation and route protection.
What's your experience with Angular Routing and Navigation? Share your tips and tricks in the comments below! π
π‘ Looking for more details? This is a condensed version of my comprehensive guide. Read the full article on my blog for additional examples, advanced patterns, troubleshooting tips, and more in-depth explanations.
If you found this guide helpful, consider checking out my other articles on Angular development and frontend development best practices.
Top comments (0)