Jet implements a modern routing architecture with lazy loading, functional guards, and type-safe route configuration.
Route Structure
Main Routes
The main application routes are defined in app.routes.ts:
export const routes: Routes = [
{ component: HomePageComponent, path: '' },
{ loadChildren: async () => (await import('./lazy.routes')).lazyRoutes, path: '' },
{
data: { case: 'not-found' },
loadComponent: async () =>
(await import('@jet/components/message-page/message-page.component')).MessagePageComponent,
path: '**',
},
];
Lazy Routes
Feature routes are lazy-loaded for optimal performance:
const mainRoutes: Routes = [];
const userRoutes: Routes = [
{
data: { case: 'email-verification-pending' },
loadComponent: async () =>
(await import('@jet/components/message-page/message-page.component')).MessagePageComponent,
path: 'email-verification-pending',
},
{
canActivate: [signedInGuard],
canDeactivate: [unsavedChangesGuard],
loadComponent: async () =>
(await import('@jet/components/profile-page/profile-page.component')).ProfilePageComponent,
path: 'profile',
},
{
canActivate: [signedOutGuard],
loadComponent: async () =>
(await import('@jet/components/reset-password-page/reset-password-page.component'))
.ResetPasswordPageComponent,
path: 'reset-password',
},
{
loadComponent: async () =>
(await import('@jet/components/settings-page/settings-page.component')).SettingsPageComponent,
path: 'settings',
},
{
loadComponent: async () =>
(await import('@jet/components/sign-in-page/sign-in-page.component')).SignInPageComponent,
path: 'sign-in',
},
{
canActivate: [signedOutGuard],
loadComponent: async () =>
(await import('@jet/components/sign-up-page/sign-up-page.component')).SignUpPageComponent,
path: 'sign-up',
},
{
canActivate: [signedInGuard],
canDeactivate: [unsavedChangesGuard],
loadComponent: async () =>
(await import('@jet/components/update-password-page/update-password-page.component'))
.UpdatePasswordPageComponent,
path: 'update-password',
},
];
export const lazyRoutes: Routes = [
{
children: [...mainRoutes, ...userRoutes],
path: '',
providers: [UserService, ProfileService]
},
];
Route Guards
Jet uses functional route guards for access control.
Signed In Guard
Restricts access to authenticated users only:
export const signedInGuard: CanActivateFn = async (
_activatedRouteSnapshot,
routerStateSnapshot,
): Promise<GuardResult> => {
const router = inject(Router);
const alertService = inject(AlertService);
const loggerService = inject(LoggerService);
const userService = inject(UserService);
let guardResult: GuardResult = router.createUrlTree(['/sign-in'], {
queryParams: { [QueryParam.ReturnUrl]: routerStateSnapshot.url },
});
try {
const { data } = await userService.getClaims();
if (data !== null) {
guardResult = true;
}
} catch (exception: unknown) {
if (exception instanceof Error) {
loggerService.logError(exception);
alertService.showErrorAlert(exception.message);
} else {
loggerService.logException(exception);
}
}
return guardResult;
};
The signedInGuard preserves the intended destination in the returnUrl query parameter for post-login redirection.
Signed Out Guard
Restricts access to unauthenticated users only:
export const signedOutGuard: CanActivateFn = async (): Promise<GuardResult> => {
const router = inject(Router);
const alertService = inject(AlertService);
const loggerService = inject(LoggerService);
const userService = inject(UserService);
let guardResult: GuardResult = router.createUrlTree(['/']);
try {
const { data } = await userService.getClaims();
if (data === null) {
guardResult = true;
}
} catch (exception: unknown) {
if (exception instanceof Error) {
loggerService.logError(exception);
alertService.showErrorAlert(exception.message);
} else {
loggerService.logException(exception);
}
}
return guardResult;
};
Unsaved Changes Guard
Prevents navigation away from forms with unsaved changes:
export const unsavedChangesGuard: CanDeactivateFn<CanComponentDeactivate> = (
component,
_activatedRouteSnapshot,
_currentRouterStateSnapshot,
nextRouterStateSnapshot,
): GuardResult => {
const translocoService = inject(TranslocoService);
if (nextRouterStateSnapshot.url.startsWith('/sign-in')) {
return true;
}
if (component.hasUnsavedChanges()) {
return confirm(translocoService.translate('confirmations.youll-lose-unsaved-changes-continue'));
}
return true;
};
Component Interface for Guards
Components that use the unsaved changes guard must implement this interface:
export interface CanComponentDeactivate {
hasUnsavedChanges: () => boolean;
}
Example implementation:
export class ProfilePageComponent implements CanComponentDeactivate, OnInit {
protected readonly profileFormGroup: FormGroup<{
full_name: FormControl<null | string>;
username: FormControl<null | string>;
}>;
public hasUnsavedChanges(): boolean {
return this.profileFormGroup.dirty;
}
}
Route Data
Routes can include custom data for component configuration:
{
data: { case: 'not-found' },
loadComponent: async () =>
(await import('@jet/components/message-page/message-page.component')).MessagePageComponent,
path: '**',
}
Route Providers
Services can be scoped to specific routes:
export const lazyRoutes: Routes = [
{
children: [...mainRoutes, ...userRoutes],
path: '',
providers: [UserService, ProfileService]
},
];
UserService and ProfileService are only instantiated when navigating to these routes, improving initial load time.
Router Configuration
The router is configured with modern features in app.config.ts:
provideRouter(
routes,
withComponentInputBinding(),
...(isDevMode() ? [withDebugTracing()] : []),
withInMemoryScrolling({ anchorScrolling: 'enabled', scrollPositionRestoration: 'enabled' }),
)
Router Features
- Component Input Binding: Route parameters and query params can be bound directly to component inputs
- Debug Tracing: Enabled in development mode for debugging navigation
- Scroll Restoration: Automatic scroll position restoration on navigation
- Anchor Scrolling: Support for fragment-based navigation
Navigation Monitoring
The app component monitors router events for global UI updates:
this.#router.events
.pipe(
filter(
(event: Event) =>
event instanceof NavigationStart ||
event instanceof NavigationCancel ||
event instanceof NavigationEnd ||
event instanceof NavigationError,
),
takeUntilDestroyed(this.#destroyRef),
)
.subscribe((event: Event) => {
if (event instanceof NavigationStart) {
this.#progressBarService.showQueryProgressBar();
return;
}
if (event instanceof NavigationEnd) {
this.activeNavigationMenuItemPath = event.url.split('?')[0];
}
if (event instanceof NavigationError) {
const error = event.error;
const message: string | undefined = error instanceof Error ? error.message : undefined;
this.#loggerService.logError(error);
this.#alertService.showErrorAlert(message);
}
this.#progressBarService.hideProgressBar();
});
Guard Best Practices
- Async Guards: Use async/await for guards that need to check authentication state
- Error Handling: Always wrap guard logic in try-catch blocks
- URL Trees: Return URL trees for redirects to preserve navigation state
- Type Safety: Use
GuardResult type for guard return values
- Dependency Injection: Use
inject() function within guard functions
- Logging: Log errors and exceptions for debugging
Guards should be fast and non-blocking. Avoid heavy computations or long-running API calls in guards.
Lazy Loading Benefits
- Reduced initial bundle size
- Faster application startup
- Better code splitting
- On-demand feature loading
- Route-scoped dependency injection