Suggest an editImprove this articleRefine the answer for “Angular router: routing, guards, and lazy loading”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Angular Router** maps URL paths to components, controls access with guards, and loads feature modules on demand with lazy loading. ```typescript { path: 'admin', canMatch: [authGuard], loadChildren: () => import('./admin') } ``` **Key point:** use `canMatch` on lazy routes, not `canActivate` - it blocks the chunk download entirely, saving bandwidth for unauthorized users.Shown above the full answer for quick recall.Answer (EN)Image**Angular Router** maps URL paths to components, controls access with guards, and loads feature modules on demand with lazy loading. ## Theory ### TL;DR - Router acts like a hotel front desk: matches the URL (room number) to a component (room), checks the token (guard) before entry, and loads floors (modules) only when a guest arrives - Core split: eager loading bundles everything at startup; lazy loading splits code into separate chunks fetched per route - Use `loadComponent` for one standalone component, `loadChildren` for a whole route file with sub-routes - Wildcard `{ path: '**' }` must always be last - it matches everything before it - `canMatch` (Angular 14+) blocks the lazy chunk download entirely; `canActivate` only blocks rendering ### Quick example ```typescript // app.routes.ts export const routes: Routes = [ { path: '', component: HomeComponent }, { path: 'admin', canMatch: [authGuard], // blocks download for unauthorized users loadChildren: () => import('./admin/admin.routes').then(m => m.ADMIN_ROUTES) }, { path: '**', redirectTo: '' } // wildcard always last ]; ``` ```html <a routerLink="/" routerLinkActive="active">Home</a> <router-outlet></router-outlet> ``` Navigate to `/admin` while logged in: the admin chunk downloads, guard passes, component renders. Navigate while logged out: guard fires, redirects to `/login`, the chunk never downloads. ### Lazy loading vs eager loading Eager loading bundles every component into one JS file at startup. A 2MB bundle ships on every first load, whether or not the user ever visits the admin panel. Lazy loading splits that into `main.js` (say, 100KB) plus `admin.js` (300KB) downloaded only when someone actually navigates to `/admin`. For any app beyond 10-15 routes, this difference in first paint time is measurable - often cutting initial load by more than half. `loadComponent` targets a single standalone component. `loadChildren` points to a route file that defines multiple sub-routes with their own components and guards. Use `loadChildren` when a feature has its own route tree. ### Route parameters ```typescript export class UserDetailComponent { private route = inject(ActivatedRoute); ngOnInit() { // Subscribe to the observable - handles re-navigation to same component this.route.paramMap.subscribe(params => { const id = params.get('id')!; this.loadUser(id); }); } } ``` `snapshot.paramMap.get('id')` reads the value once at init. If the user goes from `/users/1` to `/users/2` and Angular reuses the component instance, `ngOnInit` does not fire again and the snapshot never updates. The observable handles this correctly. ### Guards Guards run checks before a route activates. Angular 14+ replaced class-based guards with functional ones that use `inject()` directly. ```typescript // auth.guard.ts export const authGuard: CanActivateFn = (route, state) => { const authService = inject(AuthService); const router = inject(Router); return authService.isLoggedIn() ? true : router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } }); }; ``` Three guard types you will actually use: - `canActivate`: runs before the component renders. Standard auth check. - `canDeactivate`: runs when leaving a route. Use it to warn about unsaved form data. - `canMatch`: runs before the lazy chunk downloads. Better than `canActivate` for protected lazy routes because unauthorized users never download the module code. ```typescript // canDeactivate example export const unsavedChangesGuard: CanDeactivateFn<EditFormComponent> = (component) => { if (component.hasUnsavedChanges()) { return confirm('Leave without saving?'); } return true; }; ``` ### Resolvers A resolver fetches data before the route renders. The component receives the data already available via `ActivatedRoute.snapshot.data`. ```typescript // user.resolver.ts export const userResolver: ResolveFn<User> = (route) => { return inject(UserService).getUserById(route.paramMap.get('id')!); }; // routes { path: 'users/:id', component: UserDetailComponent, resolve: { user: userResolver } } // component - data is ready immediately export class UserDetailComponent { user = inject(ActivatedRoute).snapshot.data['user'] as User; } ``` The trade-off is real: resolvers hold navigation until the API responds. On a slow network, the user stares at the previous page longer. In most production apps I have seen, showing a loading skeleton inside the component works better - navigation feels instant and the fetch happens in the background. ### Nested routes ```typescript { path: 'settings', component: SettingsLayoutComponent, children: [ { path: '', redirectTo: 'profile', pathMatch: 'full' }, { path: 'profile', component: ProfileSettingsComponent }, { path: 'security', component: SecuritySettingsComponent }, ] } ``` `SettingsLayoutComponent` needs its own `<router-outlet>` for children to render inside it. The app-level outlet renders `SettingsLayoutComponent`, and the nested outlet renders whichever tab is active. ### Programmatic navigation ```typescript export class AppComponent { private router = inject(Router); goToUser(id: string) { this.router.navigate(['/users', id]); } search(term: string) { this.router.navigate(['/search'], { queryParams: { q: term, page: 1 } }); } } ``` ### Common mistakes **Reading params from snapshot in a component that handles re-navigation.** ```typescript // wrong - breaks when navigating from /users/1 to /users/2 ngOnInit() { const id = this.route.snapshot.paramMap.get('id'); } // right ngOnInit() { this.route.paramMap.subscribe(params => { this.id = params.get('id')!; }); } ``` Angular may reuse the component instance on same-route navigation. Snapshot does not update. The observable does. **Missing `pathMatch: 'full'` on empty path redirects.** ```typescript // wrong - /home also matches '' by prefix, causes redirect loops { path: '', redirectTo: 'home' } // right { path: '', redirectTo: 'home', pathMatch: 'full' } ``` The default matching strategy is prefix-based, so an empty path matches every URL. This is the most common Angular routing question on Stack Overflow. **Using `canActivate` instead of `canMatch` on lazy routes.** ```typescript // wrong - chunk downloads before guard runs { path: 'admin', loadChildren: () => import('./admin'), canActivate: [authGuard] } // right - guard fires before the download starts { path: 'admin', canMatch: [authGuard], loadChildren: () => import('./admin') } ``` **Wildcard route in the wrong position.** ```typescript // wrong - wildcard catches everything, other routes never match { path: '**', component: NotFoundComponent }, { path: 'home', component: HomeComponent }, // dead code // right { path: 'home', component: HomeComponent }, { path: '**', component: NotFoundComponent }, ``` ### Real-world usage - E-commerce checkout: `canActivate` checks if the cart has items before allowing `/checkout` - Admin dashboards: `canMatch` blocks the admin bundle download for non-admin users - Profile edit pages: `canDeactivate` prevents losing form data on accidental back navigation - Multi-step wizards: resolvers pre-load required data before the first step renders - Large SPAs (NGX-Admin pattern): nested lazy routes for paths like `/crm/clients/:id/orders` ### Follow-up questions **Q:** What is the difference between `canActivate` and `canMatch`? **A:** `canActivate` runs after the lazy module chunk has already downloaded. `canMatch` runs before. For protected lazy routes, `canMatch` is the right choice - unauthorized users never download the feature code at all. **Q:** When would you use a resolver vs loading data inside the component? **A:** Use a resolver when the page layout or title depends on data that must exist before render. Inside-component loading is better when the API is slow and you want the user to see the page skeleton immediately - navigation feels instant and the app feels faster. **Q:** How does Angular handle a browser refresh on a lazy route? **A:** The router initializes from the current URL using `APP_BASE_HREF`, re-downloads the matching lazy chunk, and runs the full activation sequence including guards. No special handling is needed unless you use SSR, where `provideClientHydration()` handles chunk matching on the client. **Q:** Why does `forRoot`/`forChild` matter in NgModule-based apps? **A:** `RouterModule.forRoot()` registers the router providers globally and must be called exactly once. `RouterModule.forChild()` adds routes without re-registering providers. Calling `forRoot` inside a lazy module creates a second router instance and breaks navigation entirely. **Q:** How do you debug route matching problems? **A:** Add `enableTracing: true` to `RouterModule.forRoot()`, or use `withDebugTracing()` in standalone config. The router logs every match step to the console - which routes were tried, what matched, and why others were skipped. ## Examples ### Basic routing with wildcard ```typescript export const routes: Routes = [ { path: '', component: HomeComponent }, { path: 'about', component: AboutComponent }, { path: 'users/:id', component: UserDetailComponent }, { path: '**', component: NotFoundComponent }, // must be last ]; ``` ```html <nav> <a routerLink="/" routerLinkActive="active">Home</a> <a routerLink="/about" routerLinkActive="active">About</a> <a [routerLink]="['/users', user.id]">View Profile</a> </nav> <router-outlet></router-outlet> ``` `routerLinkActive` adds the CSS class when the link's route matches the current URL. The `[routerLink]` array syntax handles dynamic segments cleanly - no string concatenation needed. ### Auth guard with lazy-loaded admin module ```typescript // auth.guard.ts export const authGuard: CanActivateFn = (route, state) => { const authService = inject(AuthService); const router = inject(Router); return authService.isLoggedIn() ? true : router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } }); }; // app.routes.ts export const routes: Routes = [ { path: '', component: HomeComponent }, { path: 'login', component: LoginComponent }, { path: 'admin', canMatch: [authGuard], loadChildren: () => import('./admin/admin.routes').then(m => m.ADMIN_ROUTES), }, ]; // admin/admin.routes.ts export const ADMIN_ROUTES: Routes = [ { path: '', redirectTo: 'dashboard', pathMatch: 'full' }, { path: 'dashboard', loadComponent: () => import('./dashboard.component').then(m => m.DashboardComponent), }, ]; ``` Logged-out users hitting `/admin` are redirected to `/login?returnUrl=/admin`. The admin bundle never downloads. After login, read `returnUrl` from query params and call `router.navigate([returnUrl])` to send the user to their intended destination. ### Resolver with nested routes and canDeactivate ```typescript // user.resolver.ts export const userResolver: ResolveFn<User> = (route) => { return inject(UserService).getUser(route.paramMap.get('id')!); }; // unsaved-changes.guard.ts export const unsavedChangesGuard: CanDeactivateFn<UserEditComponent> = (component) => { return component.hasUnsavedChanges() ? confirm('Leave without saving?') : true; }; // user.routes.ts export const USER_ROUTES: Routes = [ { path: ':id', component: UserShellComponent, resolve: { user: userResolver }, children: [ { path: '', redirectTo: 'overview', pathMatch: 'full' }, { path: 'overview', component: UserOverviewComponent }, { path: 'edit', component: UserEditComponent, canDeactivate: [unsavedChangesGuard] }, ], }, ]; // user-shell.component.ts export class UserShellComponent { user = inject(ActivatedRoute).snapshot.data['user'] as User; // user is available immediately, no loading state needed } ``` `UserShellComponent` renders the user name in a shared header that both child routes inherit. The resolver fetches the user before navigation completes. The `canDeactivate` guard on the edit route prompts before leaving when the form has unsaved changes.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.