Complete guide to Angular’s routing system — from basic configuration to guards, lazy loading, nested routes, and state management via the router.
Table of Contents
- Angular Router Overview
- Module 2 — Basic Configuration and Navigation
- Module 3 — Route Parameters and Query Parameters
- Module 4 — Matrix Parameters, Advanced Query Parameters, and Scrolling
- Module 5 — Nested and Named RouterOutlets
- Module 6 — Lazy Loading
- Module 7 — Route Guards
- Module 8 — State via the Router (title, data, resolve, providers)
- Quick Reference
1. Angular Router Overview
General Architecture
flowchart TD
Browser["Browser (URL change)"]
Router["Angular Router"]
Config["Routes Config\n(app.routes.ts)"]
Guards["Guards\n(CanActivate / CanMatch\nCanDeactivate)"]
Resolvers["Resolvers\n(ResolveFn)"]
Outlet["RouterOutlet\n(primary / named / nested)"]
Component["Activated Component"]
Browser --> Router
Router --> Config
Config --> Guards
Guards -->|"true"| Resolvers
Guards -->|"false / UrlTree"| Browser
Resolvers --> Outlet
Outlet --> Component
Navigation Lifecycle
sequenceDiagram
participant U as User
participant R as Router
participant CM as CanMatch Guard
participant CA as CanActivate Guard
participant Res as Resolver
participant Out as RouterOutlet
U->>R: Click RouterLink / router.navigate()
R->>R: NavigationStart event
R->>CM: Check CanMatch (filters route candidates)
CM-->>R: true / false
R->>CA: Check CanActivate
CA-->>R: true / UrlTree / RedirectCommand
R->>Res: Execute ResolveFns
Res-->>R: Resolved data → ActivatedRoute.data
R->>Out: Activate component in RouterOutlet
R->>R: NavigationEnd event
Out-->>U: Rendered view
Initial Configuration — app.config.ts
import {
PreloadAllModules,
provideRouter,
withComponentInputBinding,
withInMemoryScrolling,
withPreloading,
withViewTransitions,
} from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(
routes,
withComponentInputBinding(), // binds route params to @Input()
withInMemoryScrolling({
scrollPositionRestoration: 'enabled' // scroll to top on navigation
}),
withPreloading(PreloadAllModules), // preloads lazy chunks after initial bundle
withViewTransitions({ skipInitialTransition: true }) // CSS transitions between views
),
provideHttpClient(),
],
};
2. Module 2 — Basic Configuration and Navigation
Route Declarations
Routes are declared in a Routes array. The router traverses this array in order and stops at the first match.
// app.routes.ts
import { Routes } from '@angular/router';
export const HOME_ROUTE = 'home';
export const PRODUCTS_ROUTE = 'products';
export const routes: Routes = [
{
path: '',
redirectTo: HOME_ROUTE,
pathMatch: 'full',
},
{
path: HOME_ROUTE,
component: HomeComponent,
},
{
path: PRODUCTS_ROUTE,
children: [
{
path: '',
component: AllProductsComponent,
},
{
path: 'detail',
component: ProductDetailComponent,
},
],
},
{
path: '**', // wildcard — must be last
component: NotFoundComponent,
},
];
RouterOutlet and RouterLink in Template
<!-- app.html -->
<app-site-header />
<router-outlet /> <!-- Active route component renders here -->
<!-- navigation in a component -->
<a [routerLink]="['/', HOME_ROUTE]">Home</a>
<a routerLink="/products">Products</a>
<a [routerLink]="[PRODUCTS_ROUTE, 'detail']"
[queryParams]="{ itemId: item.id }"
routerLinkActive="active-link">Detail</a>
Programmatic Navigation
import { Router } from '@angular/router';
@Component({...})
export class HeaderComponent {
private router = inject(Router);
goToCart() {
this.router.navigate([CART_ROUTE]);
}
goToProduct(id: string) {
this.router.navigateByUrl(`/products/detail?itemId=${id}`);
}
}
Router Events
import { Router, NavigationStart, NavigationEnd } from '@angular/router';
constructor(private router: Router) {
this.router.events.subscribe(event => {
if (event instanceof NavigationStart) {
console.log('Navigation started to:', event.url);
}
if (event instanceof NavigationEnd) {
console.log('Navigation ended:', event.urlAfterRedirects);
}
});
}
3. Module 3 — Route Parameters and Query Parameters
Parameter Types
graph LR
URL["URL\n/products/detail;categoryId=pies?itemId=123#section"]
RP["Route Parameters\n:id → snapshot.params['id']"]
MP["Matrix Parameters\n;key=val → snapshot.params['key']"]
QP["Query Parameters\n?key=val → snapshot.queryParams['key']"]
FR["Fragment\n#anchor → snapshot.fragment"]
URL --> RP
URL --> MP
URL --> QP
URL --> FR
Route Parameters — Parameters in Path
// Declaration
{ path: 'products/:id', component: ProductDetailComponent }
// In the component — with withComponentInputBinding
@Component({...})
export class ProductDetailComponent {
id = input<string>(); // automatically bound to :id parameter
}
// Without withComponentInputBinding
export class ProductDetailComponent implements OnInit {
private route = inject(ActivatedRoute);
ngOnInit() {
// Snapshot (value at activation time)
const id = this.route.snapshot.paramMap.get('id');
// Observable (updates if parameter changes without recreating component)
this.route.paramMap.subscribe(params => {
const id = params.get('id');
});
}
}
Query Parameters
<a [routerLink]="[PRODUCTS_ROUTE, 'detail']"
[queryParams]="{ itemId: item.id }"
queryParamsHandling="merge">
View Detail
</a>
this.router.navigate([DETAIL_ROUTE], {
queryParams: { itemId: selectedItem.id },
queryParamsHandling: 'merge',
relativeTo: this.activatedRoute,
});
| QueryParamsHandling | Behavior |
|---|---|
'replace' (default) | Replaces all existing query params |
'merge' | Combines new with existing |
'preserve' | Keeps existing unchanged |
4. Module 4 — Matrix Parameters, Advanced Query Parameters, and Scrolling
Matrix Parameters (URL notation)
// Resulting URL: /products;categoryId=pies
<a [routerLink]="[PRODUCTS_ROUTE, { categoryId: selectedCategory }]">
{{ category.name }}
</a>
// Reading in component (same API as route params)
@Input() categoryId?: string; // via withComponentInputBinding
withInMemoryScrolling
provideRouter(routes,
withInMemoryScrolling({
scrollPositionRestoration: 'enabled',
// 'disabled' → no scroll (default)
// 'top' → always scrolls to top
// 'enabled' → top when going forward, previous position when going back
anchorScrolling: 'enabled', // enables fragment #anchor navigation
})
)
5. Module 5 — Nested and Named RouterOutlets
Nested Routes
Zipper rule: there must be exactly 1 component declared for each
RouterOutletavailable on the activated route.
graph TD
App["AppComponent\n<router-outlet> (primary)"]
Products["ProductsWrapperComponent\n<router-outlet> (nested)"]
AllProducts["AllProductsComponent"]
Detail["ProductDetailComponent"]
App -->|"/products"| Products
Products -->|"/products (path: '')"| AllProducts
Products -->|"/products/detail"| Detail
// Nested routes in app.routes.ts
{
path: PRODUCTS_ROUTE,
component: ProductsWrapperComponent, // contains a nested <router-outlet>
children: [
{
path: '',
component: AllProductsComponent,
},
{
path: DETAIL_ROUTE,
component: ProductDetailComponent,
},
],
},
Named Routes (Auxiliary / Secondary Routes)
<!-- app.html -->
<router-outlet />
<router-outlet name="cartModal" />
// Named route declaration
{
path: CART_ROUTE,
component: CartModalComponent,
outlet: 'cartModal',
}
// Navigate to named outlet
<a [routerLink]="[{ outlets: { cartModal: [CART_ROUTE] } }]">
Open Cart
</a>
// Close named outlet
this.router.navigate([{ outlets: { cartModal: null } }]);
6. Module 6 — Lazy Loading
What is Lazy Loading?
flowchart LR
Init["Initial bundle\n(downloaded on startup)"]
Chunk1["Chunk: products\n(downloaded on demand)"]
Chunk2["Chunk: pizza\n(downloaded on demand)"]
Chunk3["Chunk: login\n(downloaded on demand)"]
Init -->|"Route /products activated"| Chunk1
Init -->|"Route /pizza activated"| Chunk2
Init -->|"Route /login activated"| Chunk3
loadComponent and loadChildren
export const routes: Routes = [
// Eager (loaded in initial bundle)
{ path: HOME_ROUTE, component: HomeComponent },
// Lazy Component
{
path: LOGIN_ROUTE,
loadComponent: () =>
import('./login/login.component').then(m => m.LoginComponent),
},
// Lazy Children
{
path: PRODUCTS_ROUTE,
loadComponent: () =>
import('./products/wrapper.component').then(m => m.WrapperComponent),
loadChildren: () =>
import('./products/products.routes').then(m => m.PRODUCTS_ROUTES),
},
{
path: '**',
loadComponent: () =>
import('./not-found/not-found.component').then(m => m.NotFoundComponent),
},
];
Route Preloading
import { PreloadAllModules, withPreloading } from '@angular/router';
provideRouter(routes,
withPreloading(PreloadAllModules)
// Starts downloading lazy chunks after initial navigation
)
7. Module 7 — Route Guards
Guards Overview
flowchart TD
Nav["Navigation requested"]
CM["CanMatch\n(filters route candidates\nbefore considering them)"]
CA["CanActivate\n(controls route access)"]
CAC["CanActivateChild\n(protects child routes)"]
CD["CanDeactivate\n(prevents leaving a view)"]
Allow["Navigation allowed"]
Deny["Navigation cancelled / redirected"]
Nav --> CM
CM -->|"false → route ignored"| Nav
CM -->|"true"| CA
CA -->|"true"| CAC
CA -->|"false / UrlTree"| Deny
CAC -->|"true"| Allow
CAC -->|"false"| Deny
Allow -.->|"User navigates"| CD
CD -->|"false"| Allow
CD -->|"true"| Deny
CanActivate
// is-feature-enabled.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { map } from 'rxjs';
export const isFeatureEnabledGuard: CanActivateFn = (route, state) => {
const router = inject(Router);
const flagService = inject(FeatureFlagService);
return flagService.isFeatureEnabled$.pipe(
map(isEnabled =>
isEnabled ? true : router.createUrlTree([HOME_ROUTE])
)
);
};
// Route declaration
{
path: FEATURE_ROUTE,
canActivate: [isFeatureEnabledGuard],
loadComponent: () => import('./feature/feature.component').then(m => m.FeatureComponent),
}
CanMatch
// CanMatch runs before CanActivate — if false, route is skipped and next is tested
export const isFeatureEnabledCanMatchGuard: CanMatchFn = (route, segments) => {
const flagService = inject(FeatureFlagService);
return flagService.isFeatureEnabled$;
};
export const isUserAuthenticatedCanMatchGuard: CanMatchFn = (route, segments) => {
const authService = inject(AuthService);
return !!authService.authenticatedUser.value();
};
// Two routes on same path — CanMatch decides which one to use
{
path: HOME_ROUTE,
canMatch: [isFeatureEnabledCanMatchGuard],
component: NewHomeComponent,
},
{
path: HOME_ROUTE,
component: HomeComponent, // fallback if CanMatch returns false
},
CanDeactivate
// leave-form-can-deactivate.guard.ts
import { CanDeactivateFn } from '@angular/router';
export const leaveFormCanDeactivateGuard: CanDeactivateFn<FormComponent> = (
component,
currentRoute,
currentState,
nextState
) => {
if (!component.hasUnsavedChanges()) return true;
component.showLeaveModal.set(true);
component.nextNavigation.set(nextState.url);
return false;
};
// Declaration
{
path: '',
canDeactivate: [leaveFormCanDeactivateGuard],
loadComponent: () => import('./form/form.component').then(m => m.FormComponent),
}
CLI Generation
ng generate guard is-feature-enabled
ng g guard leave-form-can-deactivate
ng generate resolver admin-user
| Guard Type | Route Property | Typical Use Case |
|---|---|---|
CanActivateFn | canActivate | Authentication, feature flags |
CanActivateChildFn | canActivateChild | Protect all child routes |
CanMatchFn | canMatch | Choose between routes on same path |
CanDeactivateFn<T> | canDeactivate | Unsaved forms |
⚠️ Guards do not replace server-side security. Always validate permissions on the API side.
8. Module 8 — State via the Router (title, data, resolve, providers)
title — Page Title
// Static title
{ path: HOME_ROUTE, component: HomeComponent, title: "My App - Home" }
// Dynamic title via ResolveFn
{
path: `${PRODUCTS_ROUTE}/:categoryId`,
title: (route: ActivatedRouteSnapshot) => {
const category = inject(CategoriesService).getCategoryById(
route.paramMap.get('categoryId')!
);
return `My App - ${category.name}`;
},
}
data — Static Data
{
path: ABOUT_ROUTE,
component: ImageWrapperComponent,
data: {
imageUrl: '/images/about-hero.jpg',
routePath: ABOUT_ROUTE,
component: AboutComponent,
},
}
// Reading in component — via withComponentInputBinding, data is bound to @Input()
@Component({...})
export class ImageWrapperComponent {
imageUrl = input<string>('');
routePath = input<string>('');
component = input<Type<unknown>>();
}
resolve — Dynamic Data
// admin-user.resolver.ts
export const adminUserResolver: ResolveFn<User | UrlTree> = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (!authService.authenticatedUser.hasValue()) {
return new RedirectCommand(router.createUrlTree(['/', LOGIN_ROUTE]));
}
if (!authService.authenticatedUser.value()?.permissions.includes('admin')) {
return new RedirectCommand(router.createUrlTree(['/', NOT_ADMIN_ROUTE]));
}
return authService.authenticatedUser.value();
};
// Route declaration
{
path: ADMIN_ROUTE,
loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent),
resolve: { user: adminUserResolver },
// 'user' key is bound to @Input() 'user' via ComponentInputBinding
}
⚠️ Resolvers block navigation until they resolve. Do not use them for large data volumes.
providers — Route-Level Dependencies
export const MESSAGE_SERVICE = new InjectionToken<MessageService>('MessageService');
// Route declaration — isolated instance for this route and its children
{
path: PRODUCTS_ROUTE,
providers: [
{ provide: MESSAGE_SERVICE, useValue: PRODUCTS_MESSAGE }
],
loadChildren: () => import('./products/products.routes').then(m => m.PRODUCTS_ROUTES),
}
// Consumption
@Component({...})
export class ProductsComponent {
private messageService = inject(MESSAGE_SERVICE);
header = this.messageService.featureHeader;
}
9. Quick Reference
Routing Configuration Options
| Feature | API | Description |
|---|---|---|
| Basic routes | provideRouter(routes) | Required minimum |
| Component binding | withComponentInputBinding() | Binds params to @Input() |
| Scroll behavior | withInMemoryScrolling(...) | Controls scroll position |
| Preloading | withPreloading(strategy) | Preloads lazy chunks |
| View transitions | withViewTransitions(...) | CSS transitions between views |
Route Object Properties
| Property | Type | Purpose |
|---|---|---|
path | string | URL path segment |
component | Type<any> | Eager component |
loadComponent | () => Promise<Type> | Lazy component |
loadChildren | () => Promise<Routes> | Lazy child routes |
redirectTo | string | Redirect destination |
pathMatch | 'full' | 'prefix' | Path matching strategy |
children | Routes | Child routes |
canActivate | CanActivateFn[] | Activation guards |
canMatch | CanMatchFn[] | Match guards |
canDeactivate | CanDeactivateFn[] | Deactivation guards |
resolve | ResolveData | Pre-navigation data fetching |
data | Data | Static route data |
title | string | ResolveFn | Page title |
providers | Provider[] | Route-scoped DI providers |
outlet | string | Named outlet |
CLI Commands
# Generate a guard
ng generate guard guard-name
ng g guard auth --functional
# Generate a resolver
ng generate resolver resolver-name
ng g r data
Best Practices
- Export path constants (
HOME_ROUTE,PRODUCTS_ROUTE) to avoid typos - Use
loadComponentfor everything not visible on the initial view - Prefer
CanMatchoverCanActivatewhen multiple routes share the same path - Guards never replace server-side security
- Use
withComponentInputBinding()to simplify route parameter reading
Search Terms
angular · routing · navigation · frontend · development · route · parameters · configuration · data · query · router · cli · guards · lazy · loading · matrix · named · nested · providers · resolve · routes · title