Routing
This page provides best practices for routing in Angular, focusing on structure, lazy loading, and navigation.
General guidelines
Consider having a project structure similar to your routes structure, see folder structure.
Consider applying the same naming convention as your API, see API naming convention.
Consider having a client routing as close as possible to your server API routing.
Lazy loading
Consider lazy loading each feature under features folder.
Do create a sub route file for multi route features.
const routes: Routes = [
{
path: 'admin',
component: AdminPage
},
{
path: 'users',
component: BrowseUsersPage
},
{
path: 'users/:id',
component: UserProfilePage
}
// ...
];
const routes: Routes = [
// use 'loadComponent' to lazy load single route features
{
path: 'admin',
loadComponent: () => import('./features/admin/admin-page').then(c => c.AdminPage)
},
// use 'loadChildren' to lazy load multi route features.
{
path: 'users',
loadChildren: () => import('./features/users/users.routes').then(m => m.USERS_ROUTES)
}
// ...
];
const USERS_ROUTES: Routes = [
{
path: '',
component: BrowseUsersPage
},
{
path: ':id',
component: UserProfilePage
},
// ...
];
Navigation
Do use routerLink for links over router.navigate() or router.navigateByUrl().
<button (click)="showEmployees()">See employees</button>
<button (click)="showManager(user.id)">See manager</button>
import { Router } from '@angular/router';
export class CompanyPage {
#router = inject(Router);
showEmployees() {
this.#router.navigateByUrl('/users');
}
showManager(managerId: number) {
this.#router.navigate(['users', managerId]);
}
}
<a routerLink="/users">See employees</a>
<a [routerLink]="['/users', user.id]">See manager</a>
RouterLink uses standard HTML <a> tags which is better for accessibility, it supports native browser behaviors (opening link in new tab for example).
Only use router when programmatic navigation is required, such as redirects.
Data fetching
Do use withComponentInputBinding() for accessing route data (resolver, params and static data).
export class UserPage implements OnInit {
#route = inject(ActivatedRoute);
userId!: string;
user!: User;
ngOnInit(): void {
this.userId = this.route.snapshot.params['id'];
this.user = this.route.snapshot.data['user'];
}
}
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes, withComponentInputBinding()),
// ...
]
};
export class UserPage implements OnInit {
userId = input.required<string>();
user = input.required<User>();
}
Consider fetching data using a resolver instead of inside ngOnInit lifecycle hook.
export class UsersPage implements OnInit {
#userHttpClient = inject(UserHttpClient);
// 'users' is undefined until HTTP request is resolved.
users?: User[];
ngOnInit(): void {
this.#userHttpClient.getUsers().subscribe(users => this.users = users);
}
}
export class UsersPage {
// 'users' will be loaded before the component initializes
// and there is no need to handle the loading state.
users: input.required<User[]>();
}
const USERS_ROUTES: Routes = [
{
path: '',
component: UsersPage,
resolve: {
// Define your resolver here
users: () => inject(UserHttpClient).getUsers()
}
},
// other routes...
];
Using resolvers ensures that the required data is fetched before the component is initialized. This approach simplifies component logic by eliminating the need to manage loading states and subscriptions within the component itself.
Note that resolvers block navigation until data is fetched, which may not be ideal in every scenario. For better UX when dealing with non-critical data or when you want to show the page immediately, consider fetching data within the component and displaying a placeholder, skeleton, or loading indicator until the data is available.
Error handling
Do define a fallback route.
export const routes: Routes = [
...
// Keep this route at the end.
{ path: '**', component: NotFoundPage },
];
Do use withNavigationErrorHandler to handle navigation errors globally.
export const appConfig: ApplicationConfig = {
providers: [
...,
provideRouter(routes,
withNavigationErrorHandler(error => {
// Fallback to a generic error page
const router = inject(Router);
return new RedirectCommand(router.parseUrl('/error'));
})),
]
};
Do handle resolver errors by returning a RedirectCommand.
const appRoutes: Routes = [
{
path: 'post/:id',
component: PostPage,
resolve: {
post: postResolver
}
}
];
export const postResolver: ResolveFn<Post> = (route, state) => {
const postHttpClient = inject(PostHttpClient);
const router = inject(Router);
return postHttpClient.getPost(route.params['id']).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 404) {
// Redirect to a specific 'Post not found' page
const redirect = new RedirectCommand(router.parseUrl('/post-not-found'));
return of(redirect);
} else {
// Throw unhandled error further, will be caught by the global navigation error handler
return throwError(() => error);
}
})
);
};