Skip to main content
Technology & EngineeringAngular314 lines

Angular Routing

Angular Router configuration including lazy loading, guards, resolvers, and nested routes

Quick Summary27 lines
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 lines
Paste into your CLAUDE.md or agent config

Routing — 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' } without pathMatch: 'full', which matches every URL prefix and causes infinite redirect loops.

  • Guard Returning false Silently — having a guard return false without 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(...) without takeUntilDestroyed(), 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

  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.

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 false silently. When a guard returns false, the user sees nothing happen. Always redirect to a meaningful page (login, unauthorized, etc.).

  • Subscribing to route params without cleanup. ActivatedRoute observables live as long as the route is active. If the component is reused (same route, different params), the subscription persists. Use takeUntilDestroyed() or toSignal.

Install this skill directly: skilldb add angular-skills

Get CLI access →