Angular Basics #6: Router Basics
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.
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 },
];import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
],
};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.
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.”
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 pathcomponent— the component to render when that path matchesredirectTo— auto-redirect to another path. Often paired withpathMatch: '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.
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.
<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 userouterLink
Active link indication #
To highlight the current page link in the navigation bar, place routerLinkActive alongside.
<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.
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.
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
paramMapObservable — 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
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.
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.
@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.
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.
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 setupRoutesarray withpath+component(+redirectTo,**wildcard)<router-outlet>is where the matched component is drawnrouterLinkfor SPA navigation,routerLinkActivefor active state:iddynamic parameters read viainject(ActivatedRoute)+snapshot.paramMapchildrenfor nested routes — another outlet in the parent templateloadComponentfor 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.