Angular Basics #6: Router Basics

7 min read

Last time, we covered Service and dependency injection. So far, we’ve looked at what happens between components and data within a single screen, but real apps usually have multiple screens. Click a menu item, and the view changes, the URL changes, and the back button has to work. The tool that handles screen transitions is Angular’s Router.

Unlike React, which pulls in React Router as a separate external library, Angular — true to its batteries-included nature — provides the Router as an official package (@angular/router) out of the box. It’s what prompts “Would you like to add Angular routing?” when you ng new a project.

Router setup #

Modern Angular (v17+) treats the standalone pattern as the default. The Router is also set up with a single provideRouter function — no NgModule.

src/app/app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './pages/home.component';
import { AboutComponent } from './pages/about.component';

export const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'about', component: AboutComponent },
];
src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
  ],
};
src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';

bootstrapApplication(AppComponent, appConfig);

If you created the project with ng new, this structure is already set up. The only place we usually touch is app.routes.ts.

Note
In older material, you’ll often see the NgModule pattern with RouterModule.forRoot(routes) in imports. It still works but is no longer the recommended pattern. This series only covers modern Angular’s standalone + provideRouter approach.

Defining routes #

Routes is just an array of objects. Each object describes “for which path, show which component.”

src/app/app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './pages/home.component';
import { AboutComponent } from './pages/about.component';
import { NotFoundComponent } from './pages/not-found.component';

export const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: 'old-about', redirectTo: 'about', pathMatch: 'full' },
  { path: '**', component: NotFoundComponent },
];

Common properties:

  • path — the URL pattern. No leading slash (/). Empty string ('') is the root path
  • component — the component to render when that path matches
  • redirectTo — auto-redirect to another path. Often paired with pathMatch: 'full'
  • ** — wildcard. Matching is tried top-down, and this catches anything that doesn’t match elsewhere. Usually placed last for the 404 page

<router-outlet> #

When a route matches, where does its component get drawn? Right at the <router-outlet> slot. Usually, you place it in the top-level AppComponent template.

src/app/app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet, RouterLink } from '@angular/router';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, RouterLink],
  template: `
    <header>
      <a routerLink="/">Home</a>
      <a routerLink="/about">About</a>
    </header>

    <main>
      <router-outlet />
    </main>
  `,
})
export class AppComponent {}

The component matching the current URL is slotted into the <router-outlet> location. The header and footer stay; only the content in the middle swaps.

Don’t forget that RouterOutlet and RouterLink must be added directly to the imports array of the standalone component.

routerLink #

Page transition links use the routerLink directive on <a> tags instead of href.

Template snippet
<a routerLink="/">Home</a>
<a routerLink="/about">About</a>
<a [routerLink]="['/users', userId]">My profile</a>
  • Passing an absolute path as a string is the most common
  • The [routerLink]="[...]" array form is convenient for plugging in dynamic segments (/users/123)
  • Using a regular <a href="/about"> causes the browser to do a full page load, losing the SPA benefit. Always use routerLink

Active link indication #

To highlight the current page link in the navigation bar, place routerLinkActive alongside.

Template snippet
<a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">
  Home
</a>
<a routerLink="/about" routerLinkActive="active">About</a>

When the current URL matches that routerLink, the active class is added automatically. Just define a .active style in CSS and you’re done.

For a root path like /, it’s a good idea to include [routerLinkActiveOptions]="{ exact: true }". Otherwise, the link is treated as active for every sub-path (since / is effectively a prefix of every URL).

Dynamic parameters #

For paths where part of the URL changes dynamically — like a product detail or user profile — use a colon (:) prefix.

src/app/app.routes.ts
export const routes: Routes = [
  { path: 'users/:id', component: UserDetailComponent },
];

URLs like /users/123 and /users/cheolsu all match this route. Inside the component, inject ActivatedRoute to read the parameters.

src/app/pages/user-detail.component.ts
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-user-detail',
  standalone: true,
  template: `<h1>User ID: {{ userId }}</h1>`,
})
export class UserDetailComponent {
  private route = inject(ActivatedRoute);

  userId = this.route.snapshot.paramMap.get('id');
}

There are two ways to pull parameters from ActivatedRoute.

  • snapshot.paramMap.get('id') — reads the value once at the time the component was first created. Since the component is recreated on each fresh page load, this is enough in most cases
  • Subscribe to the paramMap Observable — when the same component stays alive while only the parameter changes (e.g., /users/1/users/2), the snapshot won’t pick up the new value. In that case, subscribe to the Observable
Observable subscription pattern
this.route.paramMap.subscribe(params => {
  this.userId = params.get('id');
  // Load new user data, etc.
});

For starters, snapshot alone is fine. Reach for the Observable pattern when you build screens that frequently swap parameters within the same component.

Child routes and nested outlets #

When multiple pages share a layout, or a big page has sub-sections, child routes (children) keep things clean.

src/app/app.routes.ts
export const routes: Routes = [
  {
    path: 'users/:id',
    component: UserDetailComponent,
    children: [
      { path: '', redirectTo: 'profile', pathMatch: 'full' },
      { path: 'profile', component: UserProfileComponent },
      { path: 'posts', component: UserPostsComponent },
    ],
  },
];

Place another <router-outlet> inside the parent component’s (UserDetailComponent) template.

src/app/pages/user-detail.component.ts
@Component({
  selector: 'app-user-detail',
  standalone: true,
  imports: [RouterOutlet, RouterLink, RouterLinkActive],
  template: `
    <h1>User #{{ userId }}</h1>
    <nav>
      <a routerLink="profile" routerLinkActive="active">Profile</a>
      <a routerLink="posts" routerLinkActive="active">Posts</a>
    </nav>
    <router-outlet />
  `,
})

Going to /users/123/profile keeps the parent’s header and tabs, and UserProfileComponent drops into the inner outlet. Writing routerLink="profile" without a leading slash makes it work as a relative path from the current route — handy.

Lazy loading #

By default, every route’s component is included in the initial bundle. As the app grows, the first load slows down. You can split rarely used pages with lazy loading.

src/app/app.routes.ts
export const routes: Routes = [
  { path: '', component: HomeComponent },
  {
    path: 'admin',
    loadComponent: () => import('./pages/admin.component').then(m => m.AdminComponent),
  },
];

Pass a dynamic import() to loadComponent instead of component, and the module is fetched over the network only when the user navigates to that path. Angular CLI splits it into a separate chunk automatically at build time — you only tweak the route definition.

Tip
You don’t need to obsessively apply lazy loading from the start. Write things plainly with component: first, and once the initial bundle size in the build output starts to bother you, move “occasional big screens” like admin or settings pages to lazy loading. That’s the practical approach.

Recap #

This post covered the core of Angular Router. To recap:

  • provideRouter(routes) for standalone setup
  • Routes array with path + component (+ redirectTo, ** wildcard)
  • <router-outlet> is where the matched component is drawn
  • routerLink for SPA navigation, routerLinkActive for active state
  • :id dynamic parameters read via inject(ActivatedRoute) + snapshot.paramMap
  • children for nested routes — another outlet in the parent template
  • loadComponent for lazy loading

That’s enough to build most small multi-page apps. In the next post, “Angular Basics #7: HttpClient Basics,” we’ll cover talking to a backend API. From here on, you start building apps that actually feel like apps.

X