| name | angular-routing |
| description | Angular Router (built-in `@angular/router`) — route configuration for standalone and NgModule projects, functional guards (Angular 14.1+), lazy loading, route resolvers, typed params via signals/observables, programmatic navigation, route data and meta.
Use this skill to:
- Configure routes (standalone-style or NgModule-style).
- Use functional guards (canActivate as function, preferred over class-based in 17+).
- Lazy-load components or feature modules.
- Implement auth guards via route meta + functional guards.
- Read params/queries via `inject(ActivatedRoute)` + signals or RxJS.
Do NOT use this skill for:
- General conventions (see angular-conventions).
- State management (see angular-state-and-rx).
- Forms (see angular-forms).
- Testing routes (see angular-testing).
|
Angular Routing
Angular Router is built-in (@angular/router) — no third-party. This skill covers configuration, navigation, guards, lazy loading.
Setup
Standalone (Angular 17+)
import { Routes } from '@angular/router';
import { authGuard } from './core/auth/auth.guard';
export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'login', component: LoginComponent },
{
path: 'dashboard',
loadComponent: () => import('./features/dashboard/dashboard.component').then((m) => m.DashboardComponent),
canActivate: [authGuard],
},
{
path: 'users',
loadChildren: () => import('./features/users/users.routes').then((m) => m.usersRoutes),
},
{ path: '**', component: NotFoundComponent },
];
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes, withComponentInputBinding(), withViewTransitions()),
provideHttpClient(),
],
});
provideRouter features:
withComponentInputBinding() — auto-binds route params to component @Input() / input().
withViewTransitions() — uses View Transitions API for navigation animations.
withInMemoryScrolling({ scrollPositionRestoration: 'enabled' }) — restore scroll on back nav.
withDebugTracing() — log all navigation events.
NgModule (legacy)
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [ ];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
<router-outlet> in app.component.html renders the matched component.
Route configuration
{
path: 'users',
component: UsersComponent,
data: { title: 'Users' },
resolve: { users: usersResolver },
canActivate: [authGuard],
canDeactivate: [unsavedChangesGuard],
children: [
{ path: '', component: UsersListComponent },
{ path: ':id', component: UserDetailComponent, resolve: { user: userResolver } },
{ path: ':id/edit', component: UserEditComponent, canDeactivate: [unsavedChangesGuard] },
],
}
Path matching
{ path: '', component: HomeComponent }
{ path: 'users', component: UsersComponent }
{ path: 'users/:id', component: UserDetailComponent }
{ path: 'users/:id/posts/:postId' }
{ path: 'docs/**', component: DocsComponent }
{ path: '**', component: NotFoundComponent }
{ path: 'old', redirectTo: 'new', pathMatch: 'full' }
pathMatch: 'full' for redirects — otherwise path: '' matches every URL.
Lazy loading
Standalone component:
{
path: 'dashboard',
loadComponent: () => import('./features/dashboard/dashboard.component').then((m) => m.DashboardComponent),
}
Feature routes file:
import { Routes } from '@angular/router';
import { UsersListComponent } from './users-list.component';
export const usersRoutes: Routes = [
{ path: '', component: UsersListComponent },
{ path: ':id', loadComponent: () => import('./user-detail.component').then((m) => m.UserDetailComponent) },
];
{
path: 'users',
loadChildren: () => import('./features/users/users.routes').then((m) => m.usersRoutes),
}
NgModule (legacy):
{
path: 'users',
loadChildren: () => import('./features/users/users.module').then((m) => m.UsersModule),
}
Lazy loading creates a separate JS chunk — first navigation downloads it, subsequent uses cached.
Functional guards (Angular 14.1+, preferred)
import { CanActivateFn, Router } from '@angular/router';
import { inject } from '@angular/core';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isAuthenticated()) {
return true;
}
router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
return false;
};
Apply:
{ path: 'dashboard', component: DashboardComponent, canActivate: [authGuard] }
Multiple guards
{ path: 'admin', component: AdminComponent, canActivate: [authGuard, roleGuard('admin')] }
Composed guards run in order; if any returns false / UrlTree, route blocks.
Guard factory pattern
export const roleGuard = (requiredRole: string): CanActivateFn => (route, state) => {
const auth = inject(AuthService);
return auth.hasRole(requiredRole);
};
{ canActivate: [roleGuard('admin')] }
Other guard types
| Guard | When |
|---|
canActivate | Before activating route |
canActivateChild | Before activating child routes |
canDeactivate | Before leaving route (unsaved changes prompt) |
canLoad | Before lazy-loading the route module/component (deprecated in favor of canMatch) |
canMatch | Conditionally match the route (Angular 14.2+) |
resolve | Pre-fetch data before route activates |
Class-based guards (legacy)
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree {
}
}
Still works but functional API is preferred. Don't migrate existing class guards as part of feature work unless BA spec asks.
Resolvers
Pre-fetch data before route activates. Component receives data via route.data instead of loading inside ngOnInit().
import { ResolveFn } from '@angular/router';
import { inject } from '@angular/core';
import { UsersService } from './users.service';
import type { User } from './user.model';
export const userResolver: ResolveFn<User> = (route) => {
const usersService = inject(UsersService);
const id = route.paramMap.get('id')!;
return usersService.getUser(id);
};
{ path: 'users/:id', component: UserDetailComponent, resolve: { user: userResolver } }
@Component({...})
export class UserDetailComponent {
private route = inject(ActivatedRoute);
user = this.route.snapshot.data['user'] as User;
user$ = this.route.data.pipe(map((data) => data['user'] as User));
}
Resolvers return Observable, Promise, or value. Navigation pauses until resolution completes.
Programmatic navigation
import { Router } from '@angular/router';
import { inject } from '@angular/core';
@Component({...})
export class MyComponent {
private router = inject(Router);
goToProfile(id: string) {
this.router.navigate(['/users', id]);
}
goRelative() {
this.router.navigate(['..'], { relativeTo: this.route });
}
navigateWithQuery() {
this.router.navigate(['/search'], { queryParams: { q: 'hello', page: 1 } });
}
replaceUrl() {
this.router.navigate(['/login'], { replaceUrl: true });
}
}
router.navigate(commands, extras?) — commands array; first element is path, rest are segments.
router.navigateByUrl('/users/123?tab=overview') — string URL alternative.
Reading params and queries
Snapshot (sync, only initial)
private route = inject(ActivatedRoute);
ngOnInit() {
const id = this.route.snapshot.paramMap.get('id');
const tab = this.route.snapshot.queryParamMap.get('tab') ?? 'overview';
}
Snapshot reflects route at component creation. Won't update if user navigates within same component.
Observables (reactive)
this.route.paramMap
.pipe(takeUntilDestroyed())
.subscribe((params) => {
this.id = params.get('id')!;
});
this.route.queryParamMap
.pipe(takeUntilDestroyed())
.subscribe((qp) => {
this.tab = qp.get('tab') ?? 'overview';
});
Updates on every route change within same component instance.
Signals (Angular 16+)
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs';
id = toSignal(this.route.paramMap.pipe(map((p) => p.get('id') ?? '')), { initialValue: '' });
tab = toSignal(this.route.queryParamMap.pipe(map((q) => q.get('tab') ?? 'overview')), { initialValue: 'overview' });
Use signals when consuming in computed / template:
title = computed(() => `User ${this.id()}`);
withComponentInputBinding() (Angular 16+)
provideRouter(routes, withComponentInputBinding()),
Route params auto-bind to component inputs:
@Component({ ... })
export class UserDetailComponent {
@Input() id!: string;
@Input() tab = 'overview';
}
In Angular 17.1+ with signal inputs:
id = input.required<string>();
tab = input<string>('overview');
Cleanest pattern. No need for ActivatedRoute injection in many cases.
RouterLink
<a routerLink="/users">Users</a>
<a [routerLink]="['/users', user.id]">{{ user.name }}</a>
<a [routerLink]="['/search']" [queryParams]="{ q: 'hello' }">Search</a>
<a routerLink="/dashboard" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Dashboard</a>
For external URLs, use plain <a href="..."> — routerLink is for internal nav only.
Route metadata via data
{ path: 'admin', component: AdminComponent, data: { title: 'Admin', requiresAuth: true } }
Read in component:
this.route.data.subscribe((data) => {
this.title = data['title'];
});
For app-wide title management:
this.router.events
.pipe(
filter((e) => e instanceof NavigationEnd),
takeUntilDestroyed()
)
.subscribe(() => {
let route = this.route;
while (route.firstChild) route = route.firstChild;
const title = route.snapshot.data['title'] ?? 'My App';
this.titleService.setTitle(title);
});
Or use provideRouter(routes, withRouterConfig({ paramsInheritanceStrategy: 'always' })) + Title service.
Or Angular 14.1+: title route property, set automatically:
{ path: 'admin', component: AdminComponent, title: 'Admin | My App' }
{ path: 'users/:id', component: UserDetailComponent, title: userTitleResolver }
Anti-patterns
- ❌ Forgetting
<router-outlet> in layout — routes don't render.
- ❌
<a href="/internal"> for internal nav — full reload, defeats SPA.
- ❌ Hardcoded route strings throughout app — define route constants in
app.routes.ts or shared file.
- ❌ Subscribing to
route.params without takeUntilDestroyed() — memory leak.
- ❌ Heavy logic in resolvers — slows navigation; keep resolvers thin.
- ❌ Returning
Observable<never> from a guard without UrlTree — guard hangs.
- ❌
pathMatch: 'full' missing on redirect — redirect catches every URL.
- ❌ Class-based guards in new Angular 17+ projects — functional API is cleaner.
- ❌ Multiple
<router-outlet> without named outlets — only the first activates.
- ❌ Lazy-loading single-component routes when not needed — premature optimization for small components.