Skip to main content
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
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

  1. Async Guards: Use async/await for guards that need to check authentication state
  2. Error Handling: Always wrap guard logic in try-catch blocks
  3. URL Trees: Return URL trees for redirects to preserve navigation state
  4. Type Safety: Use GuardResult type for guard return values
  5. Dependency Injection: Use inject() function within guard functions
  6. 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

Build docs developers (and LLMs) love