This episode covers Angular routing and navigation: Angular Router basics, route configuration with child routes and lazy loading, protecting routes with route guards and resolvers, and managing query params, fragments, and navigation extras.

Real applications consist of many pages: home, product list, detail, login. The Angular Router manages movement between these pages — including the URL, state, and access protection.
Episode 9 covers Angular Router basics, route configuration with child routes and lazy loading, route guards and resolvers, and query params, fragments, and navigation extras. Good routing makes an application feel whole and navigation between features smooth.
The router is enabled via provideRouter in app.config.ts, then the root component loads the active page through <router-outlet>.
import { provideRouter, Routes } from '@angular/router';
import { BerandaComponent } from './beranda/beranda.component';
export const routes: Routes = [
{ path: '', component: BerandaComponent },
{ path: 'produk', loadComponent: () =>
import('./produk/produk-list.component').then((m) => m.ProdukListComponent) },
{ path: 'detail/:id', component: DetailComponent },
{ path: '**', redirectTo: '' },
];The '' route is the default page, detail/:id uses a route parameter, and '**' is the wildcard for unknown pages. loadComponent makes the route lazy-loaded — the component's code is only downloaded when the route is accessed.
Navigation is done with RouterLink in the template or Router in the class:
<a routerLink="/produk" routerLinkActive="aktif">Daftar Produk</a>
<a routerLink="/detail/42">Buka Detail</a>routerLinkActive="aktif" adds a class while the link is active. From the class, use router.navigate(['/detail', id]) or router.navigateByUrl('/produk') for programmatic navigation, for example after login.
Pages that have sub-pages — for example an admin area with dashboard, user, and settings — are arranged with child routes:
export const routes: Routes = [
{
path: 'admin',
loadComponent: () => import('./admin/admin-layout.component')
.then((m) => m.AdminLayoutComponent),
children: [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
{ path: 'dashboard', loadComponent: () =>
import('./admin/dashboard.component').then((m) => m.DashboardComponent) },
{ path: 'user', loadComponent: () =>
import('./admin/user.component').then((m) => m.UserComponent) },
],
},
];AdminLayoutComponent renders the admin menu plus a <router-outlet> to host child pages. With loadComponent at every level, the bundle is split per feature, so the initial page is much lighter.
Lazy loading splits the application into small chunks that are downloaded on demand. The results: faster initial load, pages rarely accessed don't burden the user, and the separation of responsibilities per feature becomes clearer.
Guards control who may access a route. The modern form is a CanActivateFn function with inject:
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isLoggedIn()) {
return true;
}
return router.createUrlTree(['/login'], { queryParams: { kembali: router.url } });
};authGuard returns true if the user is logged in, or a login URL with the kembali query param so the user can be returned to the original page after login. The guard is attached to a route: { path: 'profil', component: ProfilComponent, canActivate: [authGuard] }.
A resolver fetches data before the component renders:
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { ProdukService } from './produk.service';
export const produkResolver: ResolveFn<Produk | undefined> = (route) => {
const service = inject(ProdukService);
return service.cari(Number(route.paramMap.get('id')));
};produkResolver is called before the DetailComponent activates, so the data is already available at initial render. This avoids the flickering "loading" state on detail pages.
Query params hold URL state such as search and filters:
import { inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
export class PencarianComponent {
private readonly route = inject(ActivatedRoute);
baca(): void {
this.route.queryParams.subscribe((params) => {
const q = params['q'] ?? '';
this.mulaiPencarian(q);
});
}
}To send a query param while navigating, router.navigate(['/pencarian'], { queryParams: { q: 'angular' } }). Since queryParams is an Observable, the component can react every time the user changes a filter.
A fragment is the part of a URL after #, usually for page anchors. Navigating with a fragment: router.navigate(['/dokumentasi'], { fragment: 'instalasi' }). Other useful navigation extras: state to send invisible data to the destination page, replaceUrl so the history doesn't pile up, and preserveQueryParams to keep query params when changing pages.
Key takeaways:
provideRouter(routes) enables the Angular Router; <router-outlet> displays the active page.detail/:id.loadComponent and child routes enable per-feature lazy loading.CanActivateFn protect routes from unauthorized access.In the next episode, episode 10, we'll cover state management and reactive patterns — managing application state with RxJS patterns, using BehaviorSubject and ReplaySubject as state services, getting to know NgRx or Akita, and managing side effects in a structured way.