In Angular, how do you write a functional canActivate guard that lets only admins into an /admin route and redirects everyone else?
answer
- a plain function, not a class
- the CanActivateFn signature
- inject() the auth service inside it
- return a UrlTree instead of false
basics
~10 sWrite a CanActivateFn that injects the auth service, returns true for admins and returns a UrlTree (router.createUrlTree(['/login'])) or a RedirectCommand for everyone else, then list it in the route's canActivate array.
solid answer
~40 sA functional guard is a plain function typed `CanActivateFn`: `(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => MaybeAsync<GuardResult>`, where `GuardResult` is `boolean | UrlTree | RedirectCommand` and `MaybeAsync` also allows an `Observable` or `Promise`. Inside it I call `inject(AuthService)` and `inject(Router)`, return `true` for an admin, and return `router.createUrlTree(['/login'])` for a signed-out user or `router.createUrlTree(['/forbidden'])` for a signed-in non-admin. I attach it with `canActivate: [adminGuard]` on the `admin` route. Returning a `UrlTree` makes the router cancel the current navigation and start a new one to that tree, which is cleaner than returning `false` and calling `navigate()` myself. For async checks the router takes the first emitted value. It is a UX gate only: the server must still reject non-admin API calls.
code
ts · 24 linesimport { inject } from '@angular/core';
import { CanActivateFn, Router, Routes } from '@angular/router';
import { AuthService } from './auth.service';
export const adminGuard: CanActivateFn = () => {
const auth = inject(AuthService);
const router = inject(Router);
if (!auth.isSignedIn()) {
return router.createUrlTree(['/login']);
}
if (!auth.hasRole('admin')) {
return router.createUrlTree(['/forbidden']);
}
return true;
};
export const routes: Routes = [
{
path: 'admin',
canActivate: [adminGuard],
loadComponent: () => import('./admin/admin-shell').then((m) => m.AdminShell),
},
];go deeper
Recall the CanActivateFn signature, that it returns true, false, a UrlTree or a RedirectCommand, and that it is attached with canActivate: [guard] on the route.
Explain why a returned UrlTree beats navigate() plus false, how the router treats Observable and Promise results, and why inject() works inside the function.
Show you would split signed-out and forbidden redirects, gate a whole admin section once at the parent, and keep server-side authorization as the real control.
Frame guards as a navigation UX layer over server authorization, and set team conventions for where guards live and how role checks are composed.
## What a functional route guard is In Angular's router (`@angular/router`), a **route guard** is code the router runs during a navigation to decide whether that navigation may continue. The current recommended form is a **functional guard**: a plain exported function whose type is one of the guard function types. For "may the user enter this route?" the type is `CanActivateFn`: ```ts type CanActivateFn = ( route: ActivatedRouteSnapshot, state: RouterStateSnapshot, ) => MaybeAsync<GuardResult>; ``` - `route` is the `ActivatedRouteSnapshot` of the route being activated: its params, query params, data and route config. - `state` is the `RouterStateSnapshot` of the whole target state; `state.url` is the URL the user is trying to reach. - `GuardResult` is `boolean | UrlTree | RedirectCommand`. - `MaybeAsync<T>` is `T | Observable<T> | Promise<T>`, so the guard may answer synchronously or later. The guard runs inside an **injection context** built from the route's environment injector, which is why it can call `inject(AuthService)` directly instead of receiving services through a constructor. ## The admin-only guard, step by step 1. Inject what the decision needs: an auth service and the `Router`. 2. If the user is an admin, return `true`: the navigation continues to the next check. 3. If the user is not signed in, return `router.createUrlTree(['/login'])`. 4. If the user is signed in but not an admin, return a tree to a `/forbidden` page instead, so a signed-in user is not sent back to a sign-in form they cannot use. 5. Attach the function to the route with `canActivate: [adminGuard]`. The array can hold several guards. ## What each result does | Result | Effect on the navigation | |---|---| | `true` | The navigation continues; other guards and then resolvers still run | | `false` | The navigation is cancelled and a `NavigationCancel` event is emitted | | `UrlTree` | The current navigation is cancelled and a new navigation to that tree starts | | `RedirectCommand` | Same as a `UrlTree`, plus `NavigationBehaviorOptions` for the redirect (v18+) | | `Observable` / `Promise` of the above | The router uses the **first** emitted or resolved value, then unsubscribes | Two details matter in interviews. First, when `false` cancels the very first navigation after a page load, nothing is activated in the primary `<router-outlet>`, so the user sees an empty shell. Second, the router does not keep listening to a guard's `Observable`: a later emission does not re-run or reverse the decision. ## Why return a UrlTree instead of navigate() plus false A common older pattern was `router.navigate(['/login']); return false;`. The Angular routing guide explicitly says not to do this. The reasons: - **Two navigations overlap.** Calling `navigate()` inside the guard schedules a second navigation while the first is still running; the router cancels the first as superseded, so the guard's own `false` is moot and the event stream shows a supersession rather than a redirect. - **The router cannot see the intent.** With a `UrlTree` the router records the cancellation as a redirect and hands the original `navigate()` promise over to the redirecting navigation; with `false` plus `navigate()` it sees a rejection followed by an unrelated navigation. - **History handling is lost.** A returned redirect reuses the original navigation's extras, such as `skipLocationChange` and whether to replace the history entry; an imperative call starts from defaults. ## Where this sits among the other guards - `canActivate` decides entry to one route. - `canActivateChild` on a parent decides entry to each of its child routes. - `canMatch` decides whether a route config may match the URL at all; `false` makes the router try the next route. - `canDeactivate` decides whether the user may leave the current route. For an admin area with many child pages, one guard on the `admin` parent (as `canActivate` or `canActivateChild`) avoids repeating it on every child. ## Guards are UX, not security The routing guide opens its guard chapter with a warning: never rely on client-side guards as the sole access control, because all JavaScript in the browser can be modified by the user. The admin guard stops honest users from landing on a page they cannot use; the backend must still reject every admin API call from a non-admin. ## Current recommendation versus older code Many codebases still contain `@Injectable` classes implementing the `CanActivate` interface and listed by class in `canActivate`. Passing a class or `InjectionToken` in a route's guard array has been deprecated since v15.2. New code writes a function; old classes can be adapted with `mapToCanActivate([AdminGuard])` until they are rewritten.
- What does the user see if an Angular canActivate guard returns false on the very first navigation after a page load?The router cancels the navigation and emits `NavigationCancel`, so nothing is activated in the primary `<router-outlet>`: the user sees the app shell with an empty content area. That is why a guard on an entry route should redirect with a `UrlTree` or `RedirectCommand` rather than return `false`.
- Can an Angular canActivate guard return an HttpClient Observable, and which value does the router use?Yes. The return type is `MaybeAsync<GuardResult>`, so an `Observable<boolean | UrlTree>` is fine. The router takes the first emitted value and unsubscribes; later emissions are ignored. Map the response to `true` or a `UrlTree` explicitly, and handle errors, because an error in the guard ends the navigation with a `NavigationError`.
saying these in an interview costs you the question
- Returning false and calling router.navigate('/login') is the right way to redirect from a guard.
- Guards must be @Injectable classes that implement the CanActivate interface.
- A canActivate guard on the admin route is enough to keep admin data secure.
- The router keeps subscribed to a guard Observable and re-checks on every emission.
- A guard can only answer synchronously with true or false.