Angular Routing
Read one Angular interview question at a time, then flip for the complete explanation, example, and code.
Read one Angular interview question at a time, then flip for the complete explanation, example, and code.
Angular Interview Questions · Angular Routing Companion Article
Angular applications often behave as Single Page Applications (SPAs). Instead of requesting a completely new HTML page every time the user clicks a link, Angular can change the displayed component while keeping the application loaded in the browser.
The Angular Router is responsible for connecting URLs to components and managing navigation between different parts of an application.
For example:
/ → Home page
/products → Product list
/products/101 → Product details
/admin → Admin dashboard
/login → Login page
Routing becomes especially important when an Angular application contains multiple pages, feature areas, user roles, or nested screens.
The Angular Router is the official routing library provided by Angular.
It allows an application to:
The main router features are available from:
import { Router } from '@angular/router';
A typical Angular routing flow looks like this:
User enters URL
↓
Angular Router checks route configuration
↓
Matching route is found
↓
Guards are evaluated when configured
↓
Resolvers obtain required data when configured
↓
Component is activated
↓
Component appears inside RouterOutlet
Angular needs to know which component should be displayed for each URL.
Routes are normally defined using the Routes type.
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
export const routes: Routes = [
{
path: '',
component: HomeComponent
},
{
path: 'about',
component: AboutComponent
}
];
Here:
URL Component
/ HomeComponent
/about AboutComponent
The path represents the URL path.
The component specifies which Angular component should be activated when that path matches.
Angular processes route definitions in order.
More specific routes should normally appear before broad fallback routes such as:
{
path: '**',
component: PageNotFoundComponent
}
The wildcard route should normally appear last because it matches URLs that were not matched by previous routes.
provideRouter()Standalone Angular applications can configure routing using provideRouter().
Example 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)
]
};
provideRouter() registers the services required by Angular Router and uses the supplied route configuration.
A common project structure is:
src/
└── app/
├── app.component.ts
├── app.config.ts
└── app.routes.ts
Keeping routes in app.routes.ts makes the routing configuration easier to maintain as the application grows.
Defining routes does not tell Angular where routed components should appear.
That job belongs to RouterOutlet.
<router-outlet></router-outlet>
For a standalone component, import RouterOutlet:
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
template: `
<h1>My Application</h1>
<router-outlet></router-outlet>
`
})
export class AppComponent {}
Consider these routes:
export const routes: Routes = [
{
path: '',
component: HomeComponent
},
{
path: 'products',
component: ProductsComponent
}
];
When the URL is:
/products
Angular activates ProductsComponent at the router outlet location.
A typical layout can therefore contain permanent UI around routed content:
<app-header></app-header>
<main>
<router-outlet></router-outlet>
</main>
<app-footer></app-footer>
The header and footer remain visible while the routed component changes.
Normal HTML links can use:
<a href="/products">Products</a>
Inside an Angular SPA, navigation is normally performed with RouterLink.
<a routerLink="/products">Products</a>
For standalone components:
import { RouterLink } from '@angular/router';
@Component({
imports: [RouterLink],
template: `
<a routerLink="/">Home</a>
<a routerLink="/products">Products</a>
<a routerLink="/contact">Contact</a>
`
})
export class NavbarComponent {}
RouterLink lets Angular Router handle the navigation.
Values can also be supplied dynamically.
<a [routerLink]="['/products', product.id]">
View Product
</a>
If:
product.id = 101;
the generated URL becomes:
/products/101
The array syntax is useful when a URL contains dynamic path segments.
RouterLinkActive is commonly used to highlight the currently active navigation link.
<a
routerLink="/products"
routerLinkActive="active">
Products
</a>
When /products is active, Angular adds the active CSS class.
Example CSS:
.active {
font-weight: bold;
border-bottom: 2px solid currentColor;
}
For links such as the home route, exact matching can be important.
<a
routerLink="/"
routerLinkActive="active"
[routerLinkActiveOptions]="{ exact: true }">
Home
</a>
Without exact matching, a parent URL may remain considered active while navigating to one of its descendant URLs.
Route parameters allow values to become part of a URL.
For example:
/products/101
/products/205
/products/900
Here the final segment represents a product ID.
Define the route using ::
{
path: 'products/:id',
component: ProductDetailsComponent
}
id is a route parameter.
Inject ActivatedRoute:
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-product-details',
template: `<p>Product ID: {{ productId }}</p>`
})
export class ProductDetailsComponent {
private route = inject(ActivatedRoute);
productId = this.route.snapshot.paramMap.get('id');
}
For:
/products/101
the value is:
101
A snapshot reads the current route information once.
this.route.snapshot.paramMap.get('id');
When the same component can stay active while parameters change, subscribing to parameter changes can be more appropriate.
this.route.paramMap.subscribe(params => {
const id = params.get('id');
console.log(id);
});
For example, navigating directly from:
/products/101
to:
/products/102
may reuse the same component instance, making reactive route information useful.
A route can contain multiple parameters.
{
path: 'users/:userId/orders/:orderId',
component: OrderComponent
}
Example URL:
/users/25/orders/9001
Read them using:
const userId =
this.route.snapshot.paramMap.get('userId');
const orderId =
this.route.snapshot.paramMap.get('orderId');
Route parameters are suitable for values that identify the resource represented by the route.
Query parameters appear after ? in a URL.
Example:
/products?category=laptop&page=2
Here:
category = laptop
page = 2
Unlike route parameters, query parameters generally represent optional state such as:
Create query parameters using RouterLink:
<a
routerLink="/products"
[queryParams]="{
category: 'laptop',
page: 2
}">
Laptops
</a>
Result:
/products?category=laptop&page=2
private route = inject(ActivatedRoute);
ngOnInit() {
this.route.queryParamMap.subscribe(params => {
const category = params.get('category');
const page = params.get('page');
console.log(category);
console.log(page);
});
}
Use a route parameter when the value identifies the primary resource:
/products/101
Use query parameters for optional controls:
/products?category=mobile&sort=price
This produces cleaner and more meaningful URLs.
When navigating, Angular can preserve or merge existing query parameters.
Example:
this.router.navigate(['/products'], {
queryParams: {
page: 2
},
queryParamsHandling: 'merge'
});
With merge, new query parameters are combined with existing parameters.
Another useful option is:
queryParamsHandling: 'preserve'
This keeps existing query parameters while navigating.
These options can be useful in search, filtering, pagination, and dashboard interfaces.
Large applications frequently contain screens that have their own sub-navigation.
For example:
/settings/profile
/settings/security
/settings/notifications
These routes belong logically under /settings.
They can be configured using children.
export const routes: Routes = [
{
path: 'settings',
component: SettingsComponent,
children: [
{
path: 'profile',
component: ProfileComponent
},
{
path: 'security',
component: SecurityComponent
},
{
path: 'notifications',
component: NotificationsComponent
}
]
}
];
The parent component requires its own router outlet:
<h2>Settings</h2>
<nav>
<a routerLink="profile">Profile</a>
<a routerLink="security">Security</a>
<a routerLink="notifications">Notifications</a>
</nav>
<router-outlet></router-outlet>
The outer application outlet loads SettingsComponent.
The outlet inside SettingsComponent loads its selected child.
A routing hierarchy can contain multiple levels.
Example:
/admin
/admin/users
/admin/users/101
/admin/users/101/edit
A possible configuration is:
{
path: 'admin',
component: AdminComponent,
children: [
{
path: 'users',
component: UsersComponent,
children: [
{
path: ':id',
component: UserDetailsComponent
}
]
}
]
}
Nested routes are particularly useful for applications containing:
Deep nesting should still be used carefully. An unnecessarily complicated route tree can make navigation and maintenance harder.
A parent route can redirect its empty child path to a default child.
{
path: 'settings',
component: SettingsComponent,
children: [
{
path: '',
redirectTo: 'profile',
pathMatch: 'full'
},
{
path: 'profile',
component: ProfileComponent
},
{
path: 'security',
component: SecurityComponent
}
]
}
Now:
/settings
redirects to:
/settings/profile
A redirect automatically sends navigation from one route to another.
{
path: '',
redirectTo: 'home',
pathMatch: 'full'
}
When the user visits:
/
Angular redirects to:
/home
Redirects are useful for:
pathMatch: 'full' MattersFor an empty-path redirect, pathMatch: 'full' tells the router to apply the redirect only when the complete URL path matches the empty path.
Example:
{
path: '',
redirectTo: 'dashboard',
pathMatch: 'full'
}
This prevents the empty path from unintentionally matching the beginning of other URLs.
Suppose an older application used:
/user/25
but the new URL structure is:
/users/25
The route can preserve the parameter:
{
path: 'user/:id',
redirectTo: 'users/:id'
}
Existing bookmarks and links can then continue to work.
The wildcard path is:
**
It matches routes that were not matched by earlier definitions.
Example:
{
path: '**',
component: PageNotFoundComponent
}
Complete example:
export const routes: Routes = [
{
path: '',
component: HomeComponent
},
{
path: 'products',
component: ProductsComponent
},
{
path: 'contact',
component: ContactComponent
},
{
path: '**',
component: PageNotFoundComponent
}
];
If a user enters:
/something-that-does-not-exist
Angular displays PageNotFoundComponent.
Keep wildcard routes at or near the end of the route configuration because routing uses matching order.
Pages should have meaningful browser titles.
Angular routes can define them directly.
{
path: 'products',
title: 'Products',
component: ProductsComponent
}
Another example:
{
path: 'contact',
title: 'Contact Us',
component: ContactComponent
}
When the route becomes active, the configured title can be used as the document title.
A larger application might use:
export const routes: Routes = [
{
path: '',
title: 'Home',
component: HomeComponent
},
{
path: 'products',
title: 'Products',
component: ProductsComponent
},
{
path: 'about',
title: 'About Us',
component: AboutComponent
}
];
Titles can also be resolved dynamically when the title depends on route-specific data.
For example, a product detail page might ultimately display a title based on the loaded product rather than using one fixed title for every product.
Navigation is not one single operation.
The router passes through multiple stages while processing navigation.
Angular exposes these stages through Router.events.
import {
Router,
NavigationStart,
NavigationEnd
} from '@angular/router';
private router = inject(Router);
ngOnInit() {
this.router.events.subscribe(event => {
if (event instanceof NavigationStart) {
console.log('Navigation started');
}
if (event instanceof NavigationEnd) {
console.log('Navigation completed');
}
});
}
Important router events include:
| Event | Purpose |
|---|---|
NavigationStart | Navigation begins |
RoutesRecognized | Router recognizes matching routes |
GuardsCheckStart | Guard checking begins |
GuardsCheckEnd | Guard checking completes |
ResolveStart | Resolver phase begins |
ResolveEnd | Resolver phase completes |
NavigationEnd | Navigation finishes successfully |
NavigationCancel | Navigation is cancelled |
NavigationError | Navigation fails |
RouteConfigLoadStart | Lazy route configuration begins loading |
RouteConfigLoadEnd | Lazy route configuration finishes loading |
Router events are useful for:
Applications should avoid putting unnecessary heavy work into global router-event subscriptions.
Routes can carry static custom data.
Example:
{
path: 'admin',
component: AdminComponent,
data: {
role: 'admin',
section: 'management'
}
}
Read the data through ActivatedRoute:
private route = inject(ActivatedRoute);
ngOnInit() {
this.route.data.subscribe(data => {
console.log(data['role']);
console.log(data['section']);
});
}
Route data is useful for metadata such as:
Do not use static route data as a replacement for application state that changes frequently.
Route guards control whether navigation should continue.
Typical requirements include:
User opens /dashboard
↓
Is the user authenticated?
↓
Yes → Continue
No → Redirect to login
Modern Angular applications can implement guards as functions.
A guard can return values such as:
truefalseUrlTree or RedirectCommandPromiseObservableGuards are useful for client-side navigation control, but they should not be treated as the application's security boundary.
For example, hiding an /admin route in Angular does not replace server-side authorization. Sensitive APIs and data must also be protected by the backend.
CanActivateCanActivate determines whether a particular route can be activated.
A common use is authentication.
import { inject } from '@angular/core';
import {
CanActivateFn,
Router
} from '@angular/router';
export const authGuard: CanActivateFn = () => {
const router = inject(Router);
const loggedIn = false;
if (loggedIn) {
return true;
}
return router.createUrlTree(['/login']);
};
Apply the guard:
{
path: 'dashboard',
component: DashboardComponent,
canActivate: [authGuard]
}
Now access to /dashboard passes through the guard before route activation.
A route can use multiple guards.
{
path: 'admin',
component: AdminComponent,
canActivate: [
authGuard,
adminGuard
]
}
This can separate different responsibilities such as:
authGuard → Is the user logged in?
adminGuard → Does the user have admin permission?
CanActivateChildCanActivateChild protects child routes under a parent route.
Instead of repeating the same guard:
{
path: 'admin',
canActivateChild: [adminGuard],
children: [
{
path: 'users',
component: UsersComponent
},
{
path: 'reports',
component: ReportsComponent
},
{
path: 'settings',
component: AdminSettingsComponent
}
]
}
The guard applies when activating routes inside the protected child hierarchy.
This is useful for areas such as:
/admin/users
/admin/reports
/admin/settings
CanDeactivateCanDeactivate determines whether the user can leave the current route.
A common scenario is an edit form containing unsaved changes.
User edits form
↓
User clicks another page
↓
Unsaved changes exist?
↓
Yes → Ask before leaving
No → Continue navigation
Example:
import { CanDeactivateFn } from '@angular/router';
export interface PendingChanges {
hasUnsavedChanges(): boolean;
}
export const unsavedChangesGuard:
CanDeactivateFn<PendingChanges> =
(component) => {
if (component.hasUnsavedChanges()) {
return confirm(
'You have unsaved changes. Leave this page?'
);
}
return true;
};
Configure it:
{
path: 'profile/edit',
component: EditProfileComponent,
canDeactivate: [unsavedChangesGuard]
}
Typical uses include:
CanMatchCanMatch determines whether a particular route configuration should be considered a match.
Example:
import { CanMatchFn } from '@angular/router';
export const featureGuard: CanMatchFn = () => {
const featureEnabled = true;
return featureEnabled;
};
Apply it:
{
path: 'new-dashboard',
component: NewDashboardComponent,
canMatch: [featureGuard]
}
One useful characteristic of CanMatch is that returning false can allow the router to continue checking other matching route definitions.
For example:
const routes: Routes = [
{
path: 'dashboard',
component: NewDashboardComponent,
canMatch: [newDashboardGuard]
},
{
path: 'dashboard',
component: OldDashboardComponent
}
];
This makes CanMatch useful for scenarios such as:
Older Angular learning material may mention CanLoad for some lazy-loading scenarios. Modern routing code should generally prefer CanMatch for route-matching control.
Sometimes a component needs important data before it can meaningfully render.
Without a resolver:
Open Product Page
↓
Component appears
↓
Request product
↓
Wait
↓
Display product
A resolver can move essential data retrieval into the routing process:
Navigate to Product Page
↓
Resolver loads product
↓
Navigation continues
↓
Component receives route data
Example resolver:
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { ProductService } from './product.service';
export const productResolver: ResolveFn<any> =
(route) => {
const productService =
inject(ProductService);
const id =
route.paramMap.get('id');
return productService.getProduct(id);
};
Configure it:
{
path: 'products/:id',
component: ProductDetailsComponent,
resolve: {
product: productResolver
}
}
Read the resolved data:
private route = inject(ActivatedRoute);
ngOnInit() {
this.route.data.subscribe(data => {
console.log(data['product']);
});
}
Resolvers are particularly useful when the destination route requires essential information before activation.
They should not automatically be used for every request. Non-essential data can often load after the page appears.
Large Angular applications can contain many components.
If every feature is included in the initial JavaScript required to start the application, the initial download can become unnecessarily large.
Lazy loading delays loading certain route-related code until it is needed.
Conceptually:
Without Lazy Loading
Application Start
↓
Home code
Products code
Admin code
Reports code
Settings code
↓
Application ready
With lazy loading:
Application Start
↓
Load initial application code
↓
Application ready
↓
User opens /admin
↓
Load admin feature
Lazy loading is especially useful for:
Standalone routed components can be lazy-loaded using loadComponent.
export const routes: Routes = [
{
path: 'reports',
loadComponent: () =>
import('./reports/reports.component')
.then(m => m.ReportsComponent)
}
];
The component is loaded when its route is needed instead of being eagerly referenced in the initial route configuration.
A common simple route:
{
path: 'admin',
loadComponent: () =>
import('./admin/admin.component')
.then(m => m.AdminComponent)
}
This is particularly convenient for standalone Angular applications.
A feature can also expose its own route configuration.
Main routes:
export const routes: Routes = [
{
path: 'admin',
loadChildren: () =>
import('./admin/admin.routes')
.then(m => m.ADMIN_ROUTES)
}
];
Feature routes:
import { Routes } from '@angular/router';
export const ADMIN_ROUTES: Routes = [
{
path: '',
loadComponent: () =>
import('./admin-home.component')
.then(m => m.AdminHomeComponent)
},
{
path: 'users',
loadComponent: () =>
import('./users.component')
.then(m => m.UsersComponent)
}
];
This keeps large feature areas isolated and makes the main route file easier to maintain.
The required code is loaded as part of the application's normal initial loading process.
Useful when:
Code is loaded when the associated route or feature becomes necessary.
Useful when:
Neither technique should be selected blindly. Route design should consider actual application structure and usage.
Lazy loading improves initial loading by postponing feature downloads.
However, the first visit to a lazy route may require the browser to fetch additional JavaScript.
Preloading offers a middle ground:
Application starts
↓
Initial page becomes usable
↓
Angular loads selected lazy routes in background
↓
User visits route later
↓
Required code may already be available
Angular provides built-in preloading strategies including:
NoPreloadingPreloadAllModulesNoPreloading leaves lazy routes unloaded until needed.
PreloadAllModules preloads lazy route configurations after initial navigation.
Example:
import {
provideRouter,
withPreloading,
PreloadAllModules
} from '@angular/router';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(
routes,
withPreloading(PreloadAllModules)
)
]
};
Preloading is useful when an application wants a fast initial load while also reducing delays during later navigation.
Preloading every lazy feature is not always desirable.
Suppose an application contains:
Dashboard
Products
Reports
Admin
Analytics
Settings
Maybe Products and Reports should preload, while Admin and Analytics should wait until requested.
A custom preloading strategy can make this decision.
Routes can carry metadata:
{
path: 'reports',
loadChildren: () =>
import('./reports/reports.routes')
.then(m => m.REPORT_ROUTES),
data: {
preload: true
}
}
Example strategy:
import { Injectable } from '@angular/core';
import {
PreloadingStrategy,
Route
} from '@angular/router';
import { Observable, of } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class CustomPreloadingStrategy
implements PreloadingStrategy {
preload(
route: Route,
load: () => Observable<any>
): Observable<any> {
if (route.data?.['preload']) {
return load();
}
return of(null);
}
}
Configure it:
provideRouter(
routes,
withPreloading(CustomPreloadingStrategy)
)
Custom strategies can consider factors such as:
Avoid creating complicated preloading logic unless there is a measurable reason for it.
A realistic route configuration might look like this:
import { Routes } from '@angular/router';
import { authGuard } from './guards/auth.guard';
export const routes: Routes = [
{
path: '',
title: 'Home',
loadComponent: () =>
import('./home/home.component')
.then(m => m.HomeComponent)
},
{
path: 'products',
title: 'Products',
loadComponent: () =>
import('./products/products.component')
.then(m => m.ProductsComponent)
},
{
path: 'products/:id',
loadComponent: () =>
import('./product-details/product-details.component')
.then(m => m.ProductDetailsComponent)
},
{
path: 'dashboard',
title: 'Dashboard',
canActivate: [authGuard],
loadComponent: () =>
import('./dashboard/dashboard.component')
.then(m => m.DashboardComponent)
},
{
path: 'admin',
canActivate: [authGuard],
loadChildren: () =>
import('./admin/admin.routes')
.then(m => m.ADMIN_ROUTES)
},
{
path: 'old-products',
redirectTo: 'products',
pathMatch: 'full'
},
{
path: '**',
loadComponent: () =>
import('./page-not-found/page-not-found.component')
.then(m => m.PageNotFoundComponent)
}
];
This example combines:
These concepts are related but serve different purposes.
| Feature | Example | Typical Purpose |
|---|---|---|
| Route Parameter | /products/101 | Identify a resource |
| Query Parameter | /products?page=2 | Optional filters or state |
| Fragment | /docs#routing | Identify a page section |
| Route Data | data: { role: 'admin' } | Static metadata |
| Resolver | resolve: { product: ... } | Obtain data before activation |
Choosing the correct mechanism keeps routing predictable and URLs meaningful.
Routes are configured correctly, but no routed component appears.
Check that the appropriate template contains:
<router-outlet></router-outlet>
Incorrect:
const routes: Routes = [
{
path: '**',
component: PageNotFoundComponent
},
{
path: 'products',
component: ProductsComponent
}
];
The wildcard route can capture navigation before later routes are considered.
Place it after normal routes.
pathMatch on an Empty RedirectPrefer:
{
path: '',
redirectTo: 'home',
pathMatch: 'full'
}
for a root redirect.
Less clear:
/product?id=101
For a resource-oriented application, this may be clearer:
/products/101
Query parameters are usually better suited to optional controls such as filtering and pagination.
If the component remains active while the route parameter changes, relying only on:
this.route.snapshot.paramMap.get('id');
may not represent later parameter changes.
Use reactive route information when the use case requires it.
A route guard runs in client-side application code.
An attacker should never gain access to sensitive information simply because the UI route was hidden or guarded.
Always enforce actual authorization on protected backend resources as well.
Lazy loading can reduce initial work, but splitting every tiny screen into a separate lazy route can add unnecessary complexity.
Use lazy loading where feature boundaries make sense.
Resolvers can delay route activation.
Only data that genuinely needs to be available before rendering should normally block navigation.
Secondary information can often load after the component appears.
A URL such as:
/admin/company/settings/users/permissions/groups/edit
may indicate that routing and feature boundaries have become unnecessarily complicated.
Keep route structures meaningful and maintainable.
Consider an e-commerce application.
Possible URLs:
/
/products
/products/101
/products?category=laptop
/cart
/checkout
/account/profile
/account/orders
/admin/products
/admin/orders
/login
A route tree could conceptually be organized as:
Application
│
├── Home
│
├── Products
│ └── Product Details
│
├── Cart
│
├── Checkout
│
├── Account
│ ├── Profile
│ └── Orders
│
├── Admin
│ ├── Products
│ └── Orders
│
├── Login
│
└── Page Not Found
This structure reflects actual application features rather than creating arbitrary URL levels.
Use the following practices when designing routing for larger Angular applications:
RouterLink for declarative navigation.Router.navigate() for navigation driven by application logic.CanMatch when route matching itself should be conditional.A useful way to remember routing is:
Routes
↓
Describe available URLs
RouterLink / Router.navigate()
↓
Request navigation
Router
↓
Find matching route
Guards
↓
Decide whether navigation is allowed
Resolvers
↓
Obtain required route data
RouterOutlet
↓
Display activated component
For performance-related routing:
Eager Loading
↓
Load immediately
Lazy Loading
↓
Load when required
Preloading
↓
Load lazy features in background before they are requested
For passing information:
/products/101
↓
Route Parameter
/products?page=2
↓
Query Parameter
/docs#routing
↓
Fragment
data: { role: 'admin' }
↓
Static Route Data
resolve: { product: productResolver }
↓
Resolved Data
Understanding these relationships is more important than memorizing individual router APIs.
Angular Routing is much more than switching between components. It provides the navigation architecture for a complete Angular application.
For small applications, routing may only require a few routes, RouterLink, and RouterOutlet. As the application grows, the same router can handle dynamic parameters, query parameters, nested layouts, route guards, resolvers, lazy loading, page titles, redirects, navigation events, and preloading.
The most maintainable routing configurations reflect the real structure of the application. Keep URLs meaningful, organize routes around features, lazy-load where it provides value, protect sensitive operations on the server as well as in the UI, and use each router feature for the problem it was designed to solve.