Angular Intermediate #6: Guards and Resolvers
In Basics #6 Router we covered defining routes and rendering screens into <router-outlet>. If that part answered “what to render for a given URL,” there are always two more questions hovering one step before that.
- “Is this user even allowed on this page?”
- “Wouldn’t it be better to fetch the data before showing the screen?”
The tool for the first is Guards, the tool for the second is Resolvers. Both intercept the router flow to make a decision or prepare what’s needed. In this post we cover Guards and Resolvers using modern Angular’s function-based API.
What is a Guard #
A Guard is simple. It’s a function that intercepts a route’s flow. When the user tries to navigate to a URL, it’s the checkpoint that decides “let them through, or not.” It returns one of three things.
true— pass. Continue to the intended routefalse— block. Stop the navigation and stay where you areUrlTree— redirect to another path. Likerouter.createUrlTree(['/login'])
The decision can be made synchronously, or via Promise/Observable for async results — for example, asking the server to validate a token and deciding based on the response.
Angular provides several Guard kinds depending on when they run. The three most common are canActivate, canMatch, and canDeactivate.
canActivate — the most-used guard #
canActivate runs just as the user tries to enter a particular route. 99% of auth guards live here.
Continuing the pattern from Basics #5 Service and DI, assume there’s an AuthService holding auth state — a typical singleton service that exposes isLoggedIn() and a user() signal. We’ll use it inside the guard.
Function-based guards (modern style) #
Modern Angular recommends writing guards as functions. The type is CanActivateFn, and dependencies come via inject().
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isLoggedIn()) {
return true;
}
// preserve the originally requested URL — to bring them back after login
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url },
});
};In the route definition, you list the function in the canActivate array.
export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'mypage', canActivate: [authGuard], component: MyPageComponent },
{ path: 'login', component: LoginComponent },
];canActivate is an array. You can list multiple guards, and as soon as any one returns false/UrlTree, the flow stops there. The pattern of stacking an auth guard plus a permission guard is covered shortly.
CanActivate interface) have been deprecated since v15.2. Write new code as functions; if you maintain existing class guards, see the next section.Class-based guards (legacy) #
Before functions became the standard, guards were written as classes. You’ll still see this in older code bases.
import { Injectable } from '@angular/core';
import { CanActivate, Router, UrlTree } from '@angular/router';
import { AuthService } from './auth.service';
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
constructor(private auth: AuthService, private router: Router) {}
canActivate(): boolean | UrlTree {
return this.auth.isLoggedIn() ? true : this.router.createUrlTree(['/login']);
}
}The route would say canActivate: [AuthGuard] and pass the class itself. The behavior is nearly identical to the function form, but you have one extra layer of class definition and DI boilerplate. Unlike functions, class guards are also harder to reuse outside a route context (e.g., inside another guard function).
For new projects, function-based; for maintaining existing code, class guards — split it that way.
canMatch — blocks the match itself #
canActivate asks “this route matched, may we enter?” So even for a loadComponent-based lazy route, the guard runs after the chunk has already been downloaded.
canMatch is one step earlier. Because it checks before the route matches, when the match fails the chunk isn’t downloaded; matching moves on to the next route.
import { inject } from '@angular/core';
import { CanMatchFn, Router } from '@angular/router';
import { AuthService } from './auth.service';
export const adminMatch: CanMatchFn = () => {
const auth = inject(AuthService);
const user = auth.user();
return user?.name === 'admin' ? true : inject(Router).createUrlTree(['/']);
};export const routes: Routes = [
{
path: 'admin',
canMatch: [adminMatch],
loadComponent: () => import('./pages/admin.component').then(m => m.AdminComponent),
},
// pattern for matching the same path differently per role
{ path: 'admin', component: AccessDeniedComponent },
];When canMatch returns false, that route is treated as if it doesn’t exist and matching falls through to the next one. The key benefit: users without permission don’t have to download the admin chunk. It really shines when paired with lazy loading.
If you’re stuck choosing between canActivate and canMatch, here’s a simple rule.
- Asking “may they enter?” + redirect on no →
canActivate - Don’t even want to download the lazy chunk / match the same path differently per role →
canMatch
For most auth/permission checks, canActivate is enough.
canDeactivate — checking on the way out #
canDeactivate runs when the user tries to leave a page. The “you have unsaved changes; really leave?” pattern lives here.
First, put a method on the component that says “is it OK to leave now?”
export class EditPostComponent {
private dirty = signal(false);
markDirty() { this.dirty.set(true); }
canLeave(): boolean {
if (!this.dirty()) return true;
return confirm('You have unsaved changes. Leave anyway?');
}
}The router passes the leaving component’s instance as the first argument to the guard.
import { CanDeactivateFn } from '@angular/router';
interface HasCanLeave {
canLeave: () => boolean | Promise<boolean>;
}
export const canLeaveGuard: CanDeactivateFn<HasCanLeave> = (component) => {
return component.canLeave();
};export const routes: Routes = [
{
path: 'posts/:id/edit',
canDeactivate: [canLeaveGuard],
component: EditPostComponent,
},
];The reason the guard sits on the route rather than on the component is that the router needs to make the decision right before the component is unmounted. There’s also a reuse benefit — any component that satisfies the HasCanLeave interface can plug in, so you can reuse the guard across multiple edit screens.
Resolver — pre-fetching data before entering the screen #
From here, the perspective shifts. Not “may we enter?” but “let’s fetch the data before entering.”
The basic pattern goes like this. When the user navigates to /posts/42, the router fetches the post data before the screen is rendered. The component starts the very first render with data already in hand, which cleanly cuts down on loading-spinner branches.
Assume a typical PostService with a getPost(id) method that returns Observable<Post>. A Resolver is a function of type ResolveFn.
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { PostService, Post } from './post.service';
export const postResolver: ResolveFn<Post> = (route) => {
const id = Number(route.paramMap.get('id'));
return inject(PostService).getPost(id);
};When registering on a route, you pass it as a key-value pair in the resolve object. The key name is the name the component uses to read the data.
export const routes: Routes = [
{
path: 'posts/:id',
resolve: { post: postResolver },
component: PostDetailComponent,
},
];Inside the component, read the result from ActivatedRoute’s data.
@Component({
selector: 'app-post-detail',
standalone: true,
template: `
<article>
<h1>{{ post.title }}</h1>
<p>{{ post.body }}</p>
</article>
`,
})
export class PostDetailComponent {
private route = inject(ActivatedRoute);
protected readonly post = this.route.snapshot.data['post'] as Post;
}By the time the screen renders, post is already filled in. You don’t need a “loading” branch in the component. While the Resolver is running, the router hasn’t started the route transition yet, so the user still sees the previous page. If you need a spinner during the transition, subscribe to Router events and show the indicator on NavigationStart/NavigationEnd.
@if (loading()) { ... }. Resolvers shine in narrow cases — (1) you absolutely don’t want to show an empty-data screen, (2) SEO/social previews require content on the first render, (3) the same data is consumed by both parent and child routes — so use them sparingly and intentionally.Guard vs Resolver #
Both intercept the route’s flow, but their purposes are different.
| Aspect | Guard | Resolver |
|---|---|---|
| Question asked | “May we pass through?” | “Pre-fetch data” |
| Return value | boolean / UrlTree | Data (or its Observable/Promise) |
| On failure | Block route / redirect | Route doesn’t proceed at all |
| Common usage | Auth/permission | Main data for detail pages |
The execution order is fixed too. canMatch → canActivate (parent → child) → resolve → component creation. That is, by the time the Resolver runs, all guards have already passed. Data isn’t fetched for users without permission.
Production pattern — multiple guards + Resolver on a single route #
In real apps you often hang multiple guards and a Resolver on a single route. For instance, “enter the edit page only when the user has admin rights and the post exists.”
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';
export const roleGuard = (role: string): CanActivateFn => {
return () => {
const user = inject(AuthService).user();
return user?.name === role
? true
: inject(Router).createUrlTree(['/forbidden']);
};
};Building the guard as a factory lets you slot in different roles per route. The route definition looks like this.
export const routes: Routes = [
{
path: 'admin/posts/:id/edit',
canActivate: [authGuard, roleGuard('admin')],
canDeactivate: [canLeaveGuard],
resolve: { post: postResolver },
component: EditPostComponent,
},
];Reading order maps directly to scenario.
authGuard— not logged in? Off to/loginroleGuard('admin')— not an admin? Off to/forbiddenpostResolver— pre-fetch the post data- Enter the component — the post data is already in hand on first render
- On leave,
canLeaveGuard— check for unsaved changes
Looking at a single route object, you see the page’s “entry policy + data policy + exit policy” at a glance. That’s the biggest benefit of putting guards and Resolvers at the route level — policy doesn’t get scattered inside components.
Wrapping up #
In this post we covered the two tools that intercept route flow. To summarize:
- Guard is a function that asks “should we pass through?” Returns
true/false/UrlTree canActivate— the most-used auth/permission guard- Function-based guards +
inject()are the modern Angular standard. Class guards are legacy canMatch— block the match itself, saving even the lazy chunk downloadcanDeactivate— the “unsaved, really leave?” pattern on exit- Resolver is a function that pre-fetches data before screen entry.
ResolveFn+inject() - Stack multiple guards and Resolvers on a single route to gather entry/data/exit policies at the route level
Once you have the tools to handle route policy, the next step is verifying “does this guard actually behave as intended? does the Resolver pass the right data?” In the next post, “Angular Intermediate #7 Testing,” we cover how to bind components, Services, and guards/Resolvers under unit tests — centered on TestBed and HttpTestingController.