Router Setup — Routes, RouterOutlet and Navigation

📋 Table of Contents
  1. Router Configuration
  2. Common Mistakes

Angular’s router maps browser URLs to components — when the URL is /posts/my-post, the router renders the PostDetailComponent inside the RouterOutlet. Routes are configured once in app.routes.ts and registered via provideRouter() in app.config.ts. Angular 18’s router supports component input binding — route parameters are automatically passed as @Input() properties, eliminating the need to inject ActivatedRoute in most components.

Router Configuration

// ── app.routes.ts ──────────────────────────────────────────────────────────
import { Routes } from '@angular/router';
import { authGuard, adminGuard } from '@core/guards';

export const routes: Routes = [
  // ── Redirects ──────────────────────────────────────────────────────────
  { path: '',        redirectTo: 'posts',    pathMatch: 'full' },
  { path: 'home',    redirectTo: 'posts',    pathMatch: 'full' },

  // ── Eager-loaded public routes ──────────────────────────────────────────
  {
    path:          'posts',
    loadComponent: () => import('@features/posts/post-list.component')
      .then(m => m.PostListComponent),
    title: 'All Posts',
  },
  {
    path:          'posts/:slug',        // :slug is a route parameter
    loadComponent: () => import('@features/posts/post-detail.component')
      .then(m => m.PostDetailComponent),
  },

  // ── Auth routes ────────────────────────────────────────────────────────
  {
    path:          'auth',
    loadChildren:  () => import('@features/auth/auth.routes')
      .then(m => m.authRoutes),  // load the entire auth subroutes lazily
  },

  // ── Protected admin routes ─────────────────────────────────────────────
  {
    path:         'admin',
    canActivate:  [authGuard, adminGuard],
    loadChildren: () => import('@features/admin/admin.routes')
      .then(m => m.adminRoutes),
  },

  // ── 404 wildcard ───────────────────────────────────────────────────────
  {
    path:          '**',
    loadComponent: () => import('@shared/components/not-found.component')
      .then(m => m.NotFoundComponent),
    title: '404 Not Found',
  },
];

// ── app.config.ts ──────────────────────────────────────────────────────────
export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(
      routes,
      withComponentInputBinding(),      // route params as @Input() properties
      withInMemoryScrollingOptions({
        scrollPositionRestoration: 'enabled',
        anchorScrolling: 'enabled',
      }),
      withPreloading(PreloadAllModules), // preload lazy routes in background
    ),
  ],
};

// ── PostDetailComponent — receives slug as @Input (no ActivatedRoute needed) ─
@Component({ standalone: true, template: `<h1>{{ slug }}</h1>` })
export class PostDetailComponent implements OnInit {
  @Input() slug!: string;  // bound from :slug route param automatically

  private api = inject(PostsApiService);
  post = signal<PostDto | null>(null);

  ngOnInit() {
    this.api.getBySlug(this.slug).subscribe(post => this.post.set(post));
  }
}

// ── Template navigation ────────────────────────────────────────────────────
// <a routerLink="/posts">All Posts</a>
// <a [routerLink]="['/posts', post.slug]">{{ post.title }}</a>
// <a [routerLink]="['/posts']" [queryParams]="{ category: 'tech' }">Tech</a>
// <a routerLink="/posts" routerLinkActive="active"
//    [routerLinkActiveOptions]="{ exact: true }">Posts</a>

// ── Programmatic navigation ────────────────────────────────────────────────
// this.router.navigate(['/posts', post.slug]);
// this.router.navigate(['/posts'], { queryParams: { page: 2 } });
// this.router.navigateByUrl('/posts');
Note: withComponentInputBinding() enables two powerful features: route parameters (:slug) are automatically bound to @Input() properties with the same name on the routed component, and query parameters (?page=2) are also bound to @Input() properties. This eliminates the need to inject ActivatedRoute and subscribe to paramMap in most components — the router handles the subscription and passes values directly. This is the recommended approach in Angular 18 for reading route parameters.
Tip: Use the title property on routes to set the browser tab title automatically — Angular’s TitleStrategy reads it and calls document.title. For dynamic titles (the post’s title, not a static string), provide a custom TitleStrategy: class PostTitleStrategy extends TitleStrategy { override updateTitle(snapshot: RouterStateSnapshot) { const title = this.buildTitle(snapshot); document.title = title ?? 'BlogApp'; } }. Register it as { provide: TitleStrategy, useClass: PostTitleStrategy } in app config.
Warning: The wildcard route (path: '**') must be the last route in the array. Angular matches routes from top to bottom and uses the first match. If the wildcard comes first, every URL matches it and no other routes are ever reached. Similarly, more specific routes must come before less specific ones — /posts/new must be defined before /posts/:slug or the new literal will be treated as a slug parameter.

Common Mistakes

Mistake 1 — Wildcard route not last (matches everything, other routes never reached)

❌ Wrong — { path: '**', component: NotFoundComponent } as the first route; all URLs show 404.

✅ Correct — wildcard is always the last entry in the routes array.

Mistake 2 — Missing pathMatch: ‘full’ on empty path redirects

❌ Wrong — { path: '', redirectTo: 'posts' } without pathMatch: 'full'; every URL redirects to posts because ” matches the beginning of any URL.

✅ Correct — always add pathMatch: 'full' to empty path routes.

🧠 Test Yourself

Routes are configured as: [{ path: 'posts/:slug', ... }, { path: 'posts/new', ... }]. The user navigates to /posts/new. Which component renders?