Angular Routing
Angular Router configuration including lazy loading, guards, resolvers, and nested routes
You are an expert in Angular Router for building single-page applications with lazy loading, route guards, and advanced navigation patterns.
## Key Points
- **Forgetting `pathMatch: 'full'` on Redirect Routes** — writing `{ path: '', redirectTo: 'home' }` without `pathMatch: 'full'`, which matches every URL prefix and causes infinite redirect loops.
1. **Use `loadComponent` and `loadChildren` everywhere.** Lazy loading is the default for good reason — it keeps initial bundle size small.
2. **Enable `withComponentInputBinding()`.** It eliminates the need to inject `ActivatedRoute` for reading params, query params, and resolver data.
3. **Prefer functional guards and resolvers.** They are simpler, tree-shakable, and align with Angular's move toward functions over classes.
4. **Use `createUrlTree` for guard redirects.** Returning a `UrlTree` from a guard preserves the navigation lifecycle correctly, unlike calling `router.navigate()` imperatively.
5. **Keep route files co-located with features.** A `feature.routes.ts` file next to the feature's components makes the code self-documenting.
6. **Use path aliases for deep links.** Define redirect routes to maintain backward compatibility when restructuring URLs.
- **Forgetting `pathMatch: 'full'` on redirect routes.** Without it, the empty path `''` matches every URL prefix, causing infinite redirects.
- **Order of routes matters.** The router uses first-match-wins. Place specific routes before wildcards and parameterized routes before catch-alls.
- **Resolver blocking navigation.** Resolvers delay component rendering until the observable completes. For slow APIs, show a loading state in the parent component instead of using a resolver.
- **Guard returning `false` silently.** When a guard returns `false`, the user sees nothing happen. Always redirect to a meaningful page (login, unauthorized, etc.).
## Quick Example
```html
<!-- dashboard-layout.component.html -->
<div class="dashboard">
<main><router-outlet /></main>
<aside><router-outlet name="sidebar" /></aside>
</div>
```skilldb get angular-skills/Angular RoutingFull skill: 314 linesRouting — Angular
You are an expert in Angular Router for building single-page applications with lazy loading, route guards, and advanced navigation patterns.
Core Philosophy
Angular Router is the mechanism that turns a single-page application into something that behaves like a multi-page site: URLs map to views, the back button works, deep links load the right content, and code is loaded only when needed. The router's job is not just navigation — it is the primary tool for structuring the application into independently loadable features with controlled access and pre-fetched data.
Lazy loading is not an optimization to add later; it is the default architecture. Every feature should be behind a loadComponent or loadChildren boundary. This keeps the initial bundle small, makes feature boundaries explicit in the codebase, and enables each feature to register its own providers at the route level. The cost of eager loading grows silently as the application scales, making it progressively harder to adopt lazy loading retroactively. Starting with lazy loading from day one avoids this trap.
Modern Angular routing is functional by design. Guards are plain functions, resolvers are plain functions, and routes are plain configuration arrays — no classes, no @Injectable(), no module registration. The withComponentInputBinding() feature eliminates the need to inject ActivatedRoute for reading params, query params, and resolver data, reducing component boilerplate significantly. Embracing these functional APIs makes routing code more concise, more testable, and better aligned with Angular's direction.
Anti-Patterns
-
Forgetting
pathMatch: 'full'on Redirect Routes — writing{ path: '', redirectTo: 'home' }withoutpathMatch: 'full', which matches every URL prefix and causes infinite redirect loops. -
Guard Returning
falseSilently — having a guard returnfalsewithout redirecting to a meaningful page (login, unauthorized). The user sees nothing happen and has no idea why navigation was blocked. -
Resolver Blocking Slow Navigations — using a resolver for a slow API call, which delays the entire page render until the data returns. For slow data, show a loading state in the component instead of blocking navigation with a resolver.
-
Subscribing to Route Params Without Cleanup — using
this.route.params.subscribe(...)withouttakeUntilDestroyed(), creating subscriptions that persist across navigations when the component is reused for different route parameters. -
Route Order Errors — placing a catch-all
**route or a broad parameterized route before more specific routes. Angular Router uses first-match-wins, so specific routes must come before generic ones.
Overview
The Angular Router enables navigation between views, maps URLs to components, supports lazy loading for code splitting, and provides guards and resolvers for controlling access and pre-fetching data. In modern Angular (17+), routing is configured with standalone APIs using provideRouter and functional guards/resolvers.
Core Concepts
Route Configuration
// app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{
path: 'home',
loadComponent: () =>
import('./home/home.component').then(m => m.HomeComponent),
},
{
path: 'products',
loadChildren: () =>
import('./products/product.routes').then(m => m.PRODUCT_ROUTES),
},
{ path: '**', loadComponent: () =>
import('./not-found/not-found.component').then(m => m.NotFoundComponent),
},
];
Router Setup with provideRouter
// app.config.ts
import { provideRouter, withComponentInputBinding, withViewTransitions } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(
routes,
withComponentInputBinding(), // Bind route params to @Input / input()
withViewTransitions(), // Enable View Transitions API
),
],
};
Route Parameters
// Route definition
{ path: 'products/:id', loadComponent: () => import('./product-detail.component').then(m => m.ProductDetailComponent) }
// Component using input binding (withComponentInputBinding)
@Component({ /* ... */ })
export class ProductDetailComponent {
id = input.required<string>(); // Automatically bound from :id
private productService = inject(ProductService);
product = toSignal(
toObservable(this.id).pipe(
switchMap(id => this.productService.getById(id))
)
);
}
Implementation Patterns
Lazy-Loaded Feature Routes
// products/product.routes.ts
import { Routes } from '@angular/router';
export const PRODUCT_ROUTES: Routes = [
{
path: '',
loadComponent: () =>
import('./product-layout.component').then(m => m.ProductLayoutComponent),
children: [
{
path: '',
loadComponent: () =>
import('./product-list.component').then(m => m.ProductListComponent),
},
{
path: ':id',
loadComponent: () =>
import('./product-detail.component').then(m => m.ProductDetailComponent),
},
{
path: ':id/edit',
loadComponent: () =>
import('./product-edit.component').then(m => m.ProductEditComponent),
canActivate: [authGuard],
},
],
},
];
Functional Guards
// guards/auth.guard.ts
import { CanActivateFn, Router } from '@angular/router';
import { inject } from '@angular/core';
import { AuthService } from '../services/auth.service';
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isAuthenticated()) {
return true;
}
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url },
});
};
// guards/role.guard.ts
export const roleGuard: CanActivateFn = (route) => {
const authService = inject(AuthService);
const requiredRole = route.data['role'] as string;
return authService.hasRole(requiredRole);
};
// Usage in route
{
path: 'admin',
loadComponent: () => import('./admin.component').then(m => m.AdminComponent),
canActivate: [authGuard, roleGuard],
data: { role: 'admin' },
}
Functional Resolvers
// resolvers/product.resolver.ts
import { ResolveFn } from '@angular/router';
import { inject } from '@angular/core';
import { ProductService } from '../services/product.service';
import { Product } from '../models/product.model';
export const productResolver: ResolveFn<Product> = (route) => {
const productService = inject(ProductService);
const id = route.paramMap.get('id')!;
return productService.getById(id);
};
// Route definition
{
path: ':id',
loadComponent: () => import('./product-detail.component').then(m => m.ProductDetailComponent),
resolve: { product: productResolver },
}
// Component
@Component({ /* ... */ })
export class ProductDetailComponent {
product = input.required<Product>(); // Bound from resolver via withComponentInputBinding
}
canDeactivate Guard for Unsaved Changes
export interface HasUnsavedChanges {
hasUnsavedChanges(): boolean;
}
export const unsavedChangesGuard: CanDeactivateFn<HasUnsavedChanges> = (component) => {
if (component.hasUnsavedChanges()) {
return confirm('You have unsaved changes. Leave anyway?');
}
return true;
};
Nested Layouts with Named Outlets
// Route config
{
path: 'dashboard',
loadComponent: () => import('./dashboard-layout.component').then(m => m.DashboardLayoutComponent),
children: [
{
path: '',
loadComponent: () => import('./main-panel.component').then(m => m.MainPanelComponent),
},
{
path: '',
loadComponent: () => import('./sidebar.component').then(m => m.SidebarComponent),
outlet: 'sidebar',
},
],
}
<!-- dashboard-layout.component.html -->
<div class="dashboard">
<main><router-outlet /></main>
<aside><router-outlet name="sidebar" /></aside>
</div>
Preloading Strategies
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';
// Preload all lazy routes after initial load
provideRouter(routes, withPreloading(PreloadAllModules));
// Custom preloading: only preload routes marked with data.preload
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of, EMPTY } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class SelectivePreloadStrategy implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>): Observable<any> {
return route.data?.['preload'] ? load() : EMPTY;
}
}
// Usage
{ path: 'dashboard', loadComponent: () => ..., data: { preload: true } }
provideRouter(routes, withPreloading(SelectivePreloadStrategy));
Route Transition Animations
import { provideRouter, withViewTransitions } from '@angular/router';
// Enable the View Transitions API (Chrome 111+)
provideRouter(routes, withViewTransitions({
onViewTransitionCreated: (info) => {
// Skip animation for back navigation
if (info.transition === 'back') {
info.transition = 'none';
}
},
}));
Best Practices
-
Use
loadComponentandloadChildreneverywhere. Lazy loading is the default for good reason — it keeps initial bundle size small. -
Enable
withComponentInputBinding(). It eliminates the need to injectActivatedRoutefor reading params, query params, and resolver data. -
Prefer functional guards and resolvers. They are simpler, tree-shakable, and align with Angular's move toward functions over classes.
-
Use
createUrlTreefor guard redirects. Returning aUrlTreefrom a guard preserves the navigation lifecycle correctly, unlike callingrouter.navigate()imperatively. -
Keep route files co-located with features. A
feature.routes.tsfile next to the feature's components makes the code self-documenting. -
Use path aliases for deep links. Define redirect routes to maintain backward compatibility when restructuring URLs.
Common Pitfalls
-
Forgetting
pathMatch: 'full'on redirect routes. Without it, the empty path''matches every URL prefix, causing infinite redirects. -
Order of routes matters. The router uses first-match-wins. Place specific routes before wildcards and parameterized routes before catch-alls.
-
Resolver blocking navigation. Resolvers delay component rendering until the observable completes. For slow APIs, show a loading state in the parent component instead of using a resolver.
-
Guard returning
falsesilently. When a guard returnsfalse, the user sees nothing happen. Always redirect to a meaningful page (login, unauthorized, etc.). -
Subscribing to route params without cleanup.
ActivatedRouteobservables live as long as the route is active. If the component is reused (same route, different params), the subscription persists. UsetakeUntilDestroyed()ortoSignal.
Install this skill directly: skilldb add angular-skills
Related Skills
Angular Testing
Testing Angular applications with Jest for unit tests and Cypress for end-to-end tests
Angular Dependency Injection
Angular dependency injection system including providers, injection tokens, and hierarchical injectors
Angular Forms
Reactive forms, form validation, dynamic forms, and typed form controls in Angular
Angular Ngrx
NgRx state management with store, effects, selectors, and the component store
Angular Rxjs Patterns
RxJS reactive patterns for data fetching, state management, and event handling in Angular
Angular Signals
Angular Signals for fine-grained reactivity and efficient change detection