Angular

Guards & Resolvers

15 Questions

Route guards are interfaces that let you control whether a user can navigate to or away from a route. They run before the route is activated and can allow, deny, or redirect navigation. Types include CanActivate, CanDeactivate, CanLoad, CanMatch, and Resolve. Guards can return booleans, UrlTrees, Observables, or Promises.
const routes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [authGuard],
    resolve: { user: userResolver }
  }
];
Guards are essential for authentication and authorization in Angular applications. They prevent unauthorized access to protected routes.

Why it matters: Tests understanding of access control patterns. Shows you can protect routes and implement security best practices.

Real applications: Prevent access to admin panels, redirect un authenticated users to login, check user permissions before activating routes, load data before displaying components.

Common mistakes: Developers don't implement guards at all. They implement security checks in components instead. They don't understand different guard types and use wrong one.

A CanActivate guard determines if a route can be activated. It checks conditions like authentication status and returns true to allow or a UrlTree to redirect. Inject services like AuthService using the constructor or inject() function. The guard is added to the route's canActivate array.
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
  constructor(private auth: AuthService, private router: Router) {}

  canActivate(): boolean | UrlTree {
    if (this.auth.isLoggedIn()) return true;
    return this.router.parseUrl('/login');
  }
}

// Route config
{ path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuard] }
Return a UrlTree instead of false to redirect the user to a specific page. This is cleaner than calling router.navigate() inside the guard.

Why it matters: Tests knowledge of guard implementation patterns. Shows you can protect individual routes effectively.

Real applications: Protect dashboard access, check authentication before showing profile, redirect to login if not authorized, prevent access to admin panels.

Common mistakes: Developers don't return UrlTree, instead calling router.navigate manually. They don't properly inject services. They return wrong types (not boolean/UrlTree/Observable/Promise).

Angular 15+ supports functional guards — plain functions instead of class-based guards:

export const authGuard: CanActivateFn = (route, state) => {
  const auth = inject(AuthService);
  const router = inject(Router);
  return auth.isLoggedIn() ? true : router.parseUrl('/login');
};

{ path: 'admin', canActivate: [authGuard] }
Functional guards are simpler and more readable than class-based guards. They use inject() for dependency injection and are the recommended approach in modern Angular.

Why it matters: Tests knowledge of modern Angular patterns. Shows you understand functional programming in Angular.

Real applications: Protect routes in Angular 15+ apps, implement authentication checks, manage lazy-loaded module access, create reusable guard functions.

Common mistakes: Developers continue using class-based guards in Angular 15+. They forget functional guards require inject() not constructor dependency injection. They don't understand functional guards are now the recommended approach.

CanDeactivate guards prevent users from leaving a route, typically to warn about unsaved changes. The guard receives the component instance and can call methods on it to check state. It commonly shows a confirmation dialog before navigation. This is essential for forms and editors where data loss would be frustrating.
export const unsavedChangesGuard: CanDeactivateFn<EditComponent> = (component) => {
  if (component.hasUnsavedChanges()) {
    return confirm('Discard unsaved changes?');
  }
  return true;
};
The component must implement a method like hasUnsavedChanges() that the guard can call. Return true to allow navigation or false to block it.

Why it matters: Tests understanding of how to protect unfinished work and improve user experience. Shows you prevent data loss.

Real applications: Warn users before leaving forms with unsaved changes, check for pending edits before navigation, show confirmation dialog before discarding work, prevent accidental data loss.

Common mistakes: Developers don't implement CanDeactivate for forms. They don't prompt users about unsaved changes. They don't consider user experience for destructive actions.

Resolve pre-fetches data before the route activates, ensuring the component has data when it loads. This prevents showing empty templates or loading states. The resolved data is available through ActivatedRoute.data. Functional resolvers using ResolveFn are the recommended approach.
export const userResolver: ResolveFn<User> = (route) => {
  return inject(UserService).getUser(route.paramMap.get('id')!);
};

{ path: 'user/:id', component: UserComponent, resolve: { user: userResolver } }

// In component
constructor(private route: ActivatedRoute) {
  this.user = this.route.snapshot.data['user'];
}
The component does not load until all resolvers complete. If a resolver errors, navigation is cancelled. Use catchError to handle errors gracefully.

Why it matters: Tests understanding of data pre-loading patterns. Shows you can ensure data is ready before components mount.

Real applications: Load user profile data before showing profile page, fetch product details before displaying product page, pre-populate forms with existing data, prevent showing loading states.

Common mistakes: Developers bind directly to unresolved data causing template errors. They don't handle resolver errors. They don't use resolvers and instead fetch data inside components.

CanLoad prevents lazy-loaded modules from being downloaded at all if the user does not have permission. Unlike CanActivate which only blocks activation, CanLoad blocks the entire module download. This saves bandwidth because the module bundle is never fetched. It is now deprecated in favor of CanMatch in newer Angular versions.
export const canLoadGuard: CanLoadFn = (route) => {
  const auth = inject(AuthService);
  return auth.hasPermission(route.data?.['permission']);
};

{ path: 'admin', loadChildren: () => import('./admin/admin.module'),
  canLoad: [canLoadGuard] }
Use CanMatch instead in Angular 15+ as CanLoad is deprecated. CanMatch provides the same functionality with better integration.

Why it matters: Tests knowledge of lazy loading security. Shows you understand optimization patterns for module downloads.

Real applications: Prevent admin module download for unauthorized users, block feature modules based on subscription level, optimize bundle sizes for permission-based access.

Common mistakes: Developers use CanActivate instead of CanLoad, allowing downloads before checking. They don't know CanLoad is now deprecated. They forget to use CanMatch in Angular 15+.

Yes, guards can return boolean, UrlTree, Observable<boolean | UrlTree>, or Promise<boolean | UrlTree>. Angular will wait for async results before proceeding with navigation. This allows guards to make HTTP calls to verify tokens or check server-side permissions. The async result is automatically unwrapped by the router.
export const asyncGuard: CanActivateFn = () => {
  const auth = inject(AuthService);
  const router = inject(Router);

  return auth.verifyToken().pipe(
    map(valid => valid ? true : router.parseUrl('/login')),
    catchError(() => of(router.parseUrl('/error')))
  );
};
Always handle errors with catchError in async guards. An unhandled error will cancel navigation entirely.

Why it matters: Tests understanding of asynchronous request patterns. Shows you can verify permissions server-side before allowing navigation.

Real applications: Verify authentication tokens with backend, check user permissions from API, refresh token validity before accessing protected routes, validate subscription status.

Common mistakes: Developers forget to handle errors in async guards, breaking navigation. They don't use catchError to provide fallback behavior. They don't realize async operations delay navigation.

Use the route's data property to pass metadata to guards. The guard accesses this metadata via route.data inside the function. This pattern is commonly used for role-based access control where different routes require different permissions. The data is static and defined at configuration time.
{ path: 'admin', canActivate: [roleGuard], data: { roles: ['admin'] } }

export const roleGuard: CanActivateFn = (route) => {
  const requiredRoles = route.data['roles'];
  return inject(AuthService).hasRole(requiredRoles);
};
The data property is type-safe and accessible in both guards and components. Use ActivatedRoute.data to read it in components.

Why it matters: Tests understanding of how to pass configuration to guards. Shows you can make guards reusable across multiple routes.

Real applications: Pass required roles for different routes, configure permission levels, specify resource types, customize guard behavior per route.

Common mistakes: Developers hardcode requirements in guards instead of using route data. They don't make guards reusable. They don't type-check the data property.

CanMatch (Angular 14.1+) determines if a route should even be considered during URL matching. If the guard returns false, the router skips that route and continues to the next route definition. This is different from CanActivate which runs after matching. CanMatch is useful for serving different components on the same path based on user roles.
// Different dashboards for different roles
{ path: 'dashboard', component: AdminDashboard,
  canMatch: [() => inject(AuthService).isAdmin()] },
{ path: 'dashboard', component: UserDashboard } // fallback
CanMatch replaces the deprecated CanLoad guard. It works with both eagerly and lazily loaded routes.

Why it matters: Tests knowledge of modern routing patterns. Shows you understand advanced route matching strategies.

Real applications: Show different components for same path based on user role, serve different dashboards for different user types, enable feature-specific routes conditionally.

Common mistakes: Developers don't know about CanMatch. They use CanActivate for route matching instead. They don't realize CanMatch can skip route matching entirely.

Multiple guards run in the order they are listed in the route configuration. If any guard returns false or a UrlTree, navigation is canceled or redirected immediately. All guards must return true for navigation to proceed. This allows layering of checks like authentication, role verification, and subscription status.
{ 
  path: 'admin', 
  canActivate: [authGuard, roleGuard, subscriptionGuard] 
  // All three must return true
}
Guards are evaluated sequentially, so the first failing guard stops further evaluation. Place the most common failure case first for efficiency.

Why it matters: Tests understanding of composable security patterns. Shows you can layer multiple security checks.

Real applications: Verify authentication first, then check roles, then verify subscription status all in one route. Optimize by checking cheapest guards first.

Common mistakes: Developers put expensive async guards first, causing slow performance. They don't understand sequential evaluation stops at first failure. They duplicate checks across multiple guards.

CanActivate runs after the route has been matched and decides whether to activate it. If it returns false, the user sees an error or is redirected. CanMatch decides whether the route should even be considered during URL matching. If it returns false, the router skips that route and continues looking for other matching routes. CanMatch is useful when you have the same path for different user roles.
// CanMatch: skip this route if not admin, try next match
{ path: 'dashboard', component: AdminDashboard, canMatch: [isAdminGuard] },
{ path: 'dashboard', component: UserDashboard }, // fallback

// CanActivate: route matched, but deny access
{ path: 'settings', component: SettingsComponent, canActivate: [authGuard] }
Use CanMatch when you need different components for the same URL. Use CanActivate when you want to block access entirely.

Why it matters: Tests understanding of subtle routing guard differences. Shows you know when to use each guard type.

Real applications: CanMatch determines which dashboard to serve based on role. CanActivate blocks access if user lacks permissions. Both work together for comprehensive security.

Common mistakes: Developers confuse CanMatch and CanActivate. They use CanActivate when CanMatch is more appropriate. They don't realize CanMatch handles route selection at matching phase.

A role-based guard checks whether the logged-in user has the required role to access a route. The required roles are stored in the route's data property. The guard reads the roles from the route, checks the user's roles from the auth service, and allows or denies access. If the user does not have the required role, it redirects to an unauthorized page.
export const roleGuard: CanActivateFn = (route) => {
  const auth = inject(AuthService);
  const router = inject(Router);
  const requiredRoles = route.data['roles'] as string[];

  if (auth.hasAnyRole(requiredRoles)) return true;
  return router.parseUrl('/unauthorized');
};

// Usage in routes
{ path: 'admin', component: AdminComponent,
  canActivate: [roleGuard], data: { roles: ['admin', 'superadmin'] } }
This pattern is reusable across multiple routes with different role requirements. Store roles in the route data to keep the guard generic.

Why it matters: Tests knowledge of role-based access patterns. Shows you can implement authorization systematically.

Real applications: Protect admin routes, restrict editor functions, show user-only features, implement multi-level permission hierarchies.

Common mistakes: Developers put role logic directly in the component or hardcode roles. They don't use route data for configuration. They don't make the guard generic and reusable.

The class-based Resolve interface requires creating a full injectable class with a resolve method. The functional resolver (ResolveFn) is a simpler approach introduced in Angular 15 where you write a plain function. Both serve the same purpose of pre-fetching data before a route loads. Functional resolvers are now the recommended approach because they are more concise and work well with inject().
// Functional resolver (recommended)
export const userResolver: ResolveFn<User> = (route) => {
  return inject(UserService).getUser(route.paramMap.get('id')!);
};

// Route config
{ path: 'user/:id', component: UserComponent,
  resolve: { user: userResolver } }

// Access in component
this.route.data.subscribe(data => this.user = data['user']);
Functional resolvers are easier to test and compose. They replace class-based resolvers as the preferred pattern.

Why it matters: Tests knowledge of modern patterns over legacy code. Shows you understand Angular evolution and best practices.

Real applications: Fetch user data before showing profile, load configuration before initializing app, pre-populate form data before showing edit page.

Common mistakes: Developers still use class-based Resolve interface in new projects. They don't realize functional resolvers are simpler. They try to fetch data in components instead of using resolvers.

CanDeactivate guards prevent users from accidentally leaving a page with unsaved changes. The guard calls a method on the component to check if there are pending changes. If there are, it shows a confirmation dialog. The component must implement an interface that the guard can call to check for unsaved state.
export const unsavedGuard: CanDeactivateFn<HasUnsavedChanges> = (component) => {
  if (component.hasUnsavedChanges()) {
    return confirm('You have unsaved changes. Leave anyway?');
  }
  return true;
};

// Component implements the interface
export class EditComponent implements HasUnsavedChanges {
  form = this.fb.group({ name: [''] });
  hasUnsavedChanges() { return this.form.dirty; }
}
This is especially useful for long forms and editors. The form.dirty property tracks whether the user has changed any field.

Why it matters: Tests knowledge of user experience protection. Shows you understand how to prevent accidental data loss.

Real applications: Block navigation if form has unsaved changes, warn before leaving a multi-step wizard, prevent leaving a page with pending uploads, protect against accidental browser back.

Common mistakes: Developers don't implement CanDeactivate and lose user data. They trigger CanDeactivate for every navigation (inefficient). They don't let users bypass the guard when they confirm they want to leave.

Yes, all guards can return boolean, UrlTree, Observable of boolean or UrlTree, or Promise of boolean or UrlTree. This is important because many guard checks require async operations like calling an API to verify a token or checking permissions from a server. Angular waits for the Observable or Promise to resolve before proceeding with or cancelling navigation.
export const authGuard: CanActivateFn = () => {
  const auth = inject(AuthService);
  const router = inject(Router);

  return auth.checkToken().pipe(
    map(valid => valid ? true : router.parseUrl('/login')),
    catchError(() => of(router.parseUrl('/login')))
  );
};
Return a UrlTree instead of false to redirect the user. Always use catchError to prevent unhandled errors from blocking navigation.

Why it matters: Tests knowledge of async patterns in routing. Shows you understand how Angular handles asynchronous guard decisions.

Real applications: Verify authentication with backend API, check token validity with server, verify permissions from remote service, wait for data before allowing navigation.

Common mistakes: Developers forget to return Observable or Promise instead of boolean. They don't handle errors in async guards. They don't use catchError, causing unhandled errors. They return plain values instead of wrapped Observables.