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.
@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.
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.
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.
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+.
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.
{ 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.
// 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.
{
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.
// 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.
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.
// 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.
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.
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.