Run one experiment, @name, before you read a single line of theory. Log out of your application, then type
/admin into the browser's address bar and press enter. The command post opens without so much as a blink. The router did exactly what you taught it to do: it took the text from the bar, found the matching entry in the routes array and dropped the component into the outlet. The question "who are you, and are you allowed through here" was never asked, because nowhere in that machine is there yet a place where it could be asked.Do not count on hiding the link in the navigation being enough. A hidden link disappears from view, but the address stays, and an address can be typed from memory, pulled out of bookmarks or received from somebody in a message. Protection you can walk around by typing six characters on a keyboard is not protection.
In the Academy nobody leaves a gate unmanned. The wall has one opening, and in that opening stands a guard: he stops the newcomer, inspects the seal on their scroll and makes one of two decisions - let them through or turn them back. He does not render views, he does not fetch supplies, he does not decorate the courtyard. He answers yes or no. In Angular such a sentry is called a guard, and in this lesson you will post three of them: at the main gate one who checks whether the newcomer is logged in at all, at the inner gate one who looks at rank, and at the postern one who lets nobody out with unfinished work.
A guard is a function you never call yourself. The calling belongs to the router and happens at a very precise moment: the address has already been matched to an entry in the routes array, but the component does not exist yet and nothing has reached the screen. The router asks the guard for its opinion and looks at what the function returned. True means "let them through" and the journey continues. False means "turn them back": the navigation is cancelled, the component is never created, and the address in the bar returns to the previous one.
So that TypeScript knows what shape of function to expect here, the
@angular/router package exports a ready-made type alias named CanActivateFn. A type alias is a name given in advance to a certain shape - in this case to a function with two parameters that returns a decision. When you sign your constant with it after a colon, you do not have to write a single annotation inside: TypeScript infers the types of both parameters on its own, and it checks the return type on its own too.Memorise that name letter by letter, because two others that sound just as sensible simply do not exist. The
@angular/router package exports nothing called GuardFn and nothing called ActivateGuard - I checked both imports with the TypeScript compiler on Angular 19, and both end with the same error about no such export. What does exist is the name CanActivate without the ending, and that is the real trap, because it belongs to the old variant: a decorated class that implemented an interface of that name and went into the route as an injection token. Functional guards arrived in Angular 14, and in the router's own type declarations a class handed to a route now goes in through an alias literally named DeprecatedGuard - Angular itself labels that road as the old one. Functions are what we will stick to here. So you annotate the constant with CanActivateFn, never with CanActivate.Let us start with a guard that lets everybody through - so that you first see the construction itself, with no decision inside it. We will name the file the way Angular does it by convention, after the role and after the type.
1// guards/auth.guard.ts
2import { CanActivateFn } from '@angular/router';
3
4export const authGuard: CanActivateFn = () => {
5 return true;
6};This guard is not watching over anything yet, because it returns plain truth, and more importantly it will not be called even once for now: the file sits in the project and the router knows nothing about it. Pay attention to what is not here. There is no class, no decorator, no
new keyword - it is an ordinary constant holding an arrow function, exactly like the ones you wrote in JavaScript. There is no annotation on the parameters or on the result either, because all that knowledge was carried in by the CanActivateFn standing after the colon. The empty parentheses at the arrow are perfectly valid: since you are not using the arguments, you do not have to spell them out, and TypeScript lets you supply a function with fewer parameters wherever a function with more is expected. We will reach for them in a moment, and then they will appear.A guard on his own will stop nobody until you order him to stand in one specific opening. You point out the place in the route entry, with the
canActivate field. That field takes an array, and not out of politeness: several guards may stand at one gate, each responsible for something different. The router calls them in the order they are written and lets the newcomer through only once they all say yes; the first refusal ends the matter and the rest are not asked.Look closely at the order of tokens in this one line, because you will be rebuilding it token by token: first the opening brace together with the path, then the component, then the array of guards, and at the very end the closing brace of the entry. The imports matter too - you bring in the guard constant exactly as you bring in the component, because it is simply another export from your own project.
1// app.routes.ts
2import { Routes } from '@angular/router';
3import { AdminComponent } from './admin/admin.component';
4import { authGuard } from './guards/auth.guard';
5
6export const routes: Routes = [
7 { path: 'admin', component: AdminComponent, canActivate: [authGuard] }
8];From now on every entry to
/admin passes through the guard - who still lets everybody through, because inside he has nothing but return true, but who is already taking part in the route's lifecycle. The most important detail sits in the array: there are no parentheses after the name authGuard. You pass the function itself, not its result. Had you written [authGuard()] there, you would be trying to call the guard immediately, at the moment the routes file is loaded, and its result would land in the array instead of the guard itself. That particular slip TypeScript will catch for you - I checked it with the compiler: it replies that the function expected two arguments and got zero. Notice as well what was left untouched: AdminComponent. The component does not know that anybody is watching over it, it holds not a single line checking permissions, and it should not - the decision is made before it, at the route level.A guard who says yes to everybody is decoration and nothing more. A real one has to ask somebody about the state of things, and the knowledge of who is logged in lies neither in the router nor in the component. Let us agree that we have our own
AuthService in the project with two methods: isLoggedIn() answers true or false to the question of being logged in, and hasRole() takes the name of a rank and says whether the newcomer holds it. You will build the insides of such a service in the module on the craftsmen's guilds; here, treat it as a sealed box with two questions.You reach for the service with the
inject() function from the @angular/core package, the same one you used in the previous lesson to ask for the scroll of the active route. There is a condition here worth knowing about right away: inject() may only be called in what is known as an injection context, that is, in a place where Angular has its injector at hand. The body of a guard is such a place, because it is the router that calls this function and prepares that context itself. The call has to be synchronous, though - right in the body of the guard, not inside a function fired later - which is why we write inject() in the opening lines, before any logic begins.1// guards/auth.guard.ts
2import { inject } from '@angular/core';
3import { CanActivateFn } from '@angular/router';
4import { AuthService } from '../services/auth.service';
5
6export const authGuard: CanActivateFn = () => {
7 const auth = inject(AuthService);
8 return auth.isLoggedIn();
9};The gate has finally started filtering: a logged-in visitor walks through, a logged-out one is turned back and
AdminComponent is never created at all. What did not change - and that is the most interesting part here - is the route entry, which looks exactly as it did a moment ago; the guard's type is the same as well, and the constant is still called authGuard, so the routes file needs not a single correction. Only the inside of the function changed: instead of a constant truth you now return the answer to a question put to the service.There remains, however, one thing that will spoil the experience for the user. When
isLoggedIn() returns false, the visitor sees... nothing. The screen they were standing on stays where it was, the address goes back to the previous one, no message appears. From their point of view the click simply did not work, and there is no way to tell a refusal from a broken button.A guard at a real gate does not stay silent - he sends the newcomer off to the chancery for a seal. A guard in Angular can do exactly the same, because it is allowed to return more than a boolean. It may hand back an object of type
UrlTree, that is, a finished address taken apart into its pieces: path segments, query parameters, fragment. To the router such a return means "do not let them in here, move them there instead", and it is one operation, not two. It may also return a stream or a promise of such an answer, when the decision requires asking the server - the router then waits for the result.You build a
UrlTree with the createUrlTree() method of the Router service, the same service you used in the previous lesson to issue the order to march. The method takes the same arguments as navigate(): an array of segments and an optional options object with a queryParams field. The difference is fundamental and worth keeping in the back of your mind: navigate() performs the transition straight away, while createUrlTree() only builds a description of the address and hands it to you. I recommend returning a UrlTree, @name, and I advise against calling navigate() inside a guard: a navigate() call starts a second navigation while the first one has not finished yet, and two journeys running at once can leave the address of the losing one in the bar.To the login address we will add one more note: the address the visitor was trying to reach, so that after logging in they land back where they started. We take it from the guard's second parameter, so it is finally time to spell both of them out. The first parameter, traditionally named
route, is the snapshot of the target route - the same kind of object you read paramMap from. The second, state, is the snapshot of the router's entire state, and in it the url field holding the full address the navigation was heading for.1// guards/auth.guard.ts
2import { inject } from '@angular/core';
3import { CanActivateFn, Router } from '@angular/router';
4import { AuthService } from '../services/auth.service';
5
6export const authGuard: CanActivateFn = (route, state) => {
7 const auth = inject(AuthService);
8 const router = inject(Router);
9
10 if (auth.isLoggedIn()) {
11 return true;
12 }
13
14 // turn them back to the login screen and remember where they were heading
15 return router.createUrlTree(['/login'], {
16 queryParams: { returnUrl: state.url }
17 });
18};I ran this guard on a real Angular 19 router. A logged-out visitor who types
/admin lands at the address /login?returnUrl=%2Fadmin - the slash in the note is encoded, because the router encodes special characters in query parameters by itself, and that is exactly the value the login screen will read through queryParamMap. A logged-in visitor goes through to /admin with no change in behaviour at all. Neither the routes array nor the guard's type changed along the way: CanActivateFn has allowed all three kinds of answer all along, and only now are we reaching for the second one. Notice that the branch with return true stands before the redirect and ends the function - that is the typical shape of a guard: first release the permitted case, then deal with the refusal.Logged in does not yet mean authorised. The shogun enters the command post, the smith enters the armoury, and an ordinary student enters neither - and now it might look as though you need a separate guard for every rank. You do not. One is enough, as long as it receives in its orders the name of the rank it is to watch over.
To carry such an order, the route entry has a
data field. It is an ordinary object with your own keys and values, attached to the route configuration and visible afterwards in the snapshot. There is one name for it and there are no substitutes: Route knows no meta field, no config field and no params field - parameters come from the address, not from the configuration, so there is nothing of the sort to write in the routes file. I put each of those three names into a route and compiled it: all three end with the same TypeScript message that no such property exists on type Route.So let us post two guards at the gate at once and add their orders. The order inside the array is not accidental: first we check the login, and only then the rank, because asking about the rank of somebody anonymous makes no sense.
1// app.routes.ts, a fragment of the routes array
2{
3 path: 'admin',
4 component: AdminComponent,
5 canActivate: [authGuard, roleGuard],
6 data: { role: 'shogun' }
7}The entry grew by two lines and by no new concept whatsoever:
canActivate is still the same array, only with a second name inside it, and data is an ordinary object that the router will carry along and hand to the guard. AdminComponent itself, once again, knows nothing about any of it. What matters is not to confuse the levels: data is a field of the route entry, a sibling of path and component, and not a field inside canActivate.On the other side the guard has to read that order, and everything it needs for the job arrives in the first parameter. The route snapshot has a
data field holding exactly the object you wrote in the configuration, so you take the rank out with route.data['role']. Three other spellings that come to mind in this spot are wrong for three different reasons, and each one is worth knowing. route.params['role'] will compile without a word of complaint, but it looks into the path parameters, that is, where the segments of the address marked with a colon land; there is no :role in the path, so you get undefined and a silent bug. route.query['role'] will not compile at all, because the snapshot has no field named query - query parameters sit in queryParams and queryParamMap, but that is not the right place anyway, because we are after an order from the configuration, not a note from the address. route.role will not compile either: the snapshot has no field named after your key, because your keys live inside the data object. I pushed both of those attempts through the compiler and both end with an error about a property that does not exist.The values in
data carry the loosest possible type, because Angular has no idea what you are going to put there. To go on working with the rank as text, we will add as string - an assurance for TypeScript, called a type assertion, which says "I know what is in here, treat it as a string".1// guards/role.guard.ts
2import { inject } from '@angular/core';
3import { CanActivateFn, Router } from '@angular/router';
4import { AuthService } from '../services/auth.service';
5
6export const roleGuard: CanActivateFn = (route) => {
7 const auth = inject(AuthService);
8 const router = inject(Router);
9 const requiredRole = route.data['role'] as string;
10
11 if (auth.hasRole(requiredRole)) {
12 return true;
13 }
14
15 return router.createUrlTree(['/forbidden']);
16};That same guard will now serve every gate in the Academy - at the armoury you write
data: { role: 'smith' } and you do not write a single new line of code. I ran both routes at once: a visitor with the rank of shogun enters /admin, and one without it lands at /forbidden, with the address /admin never even flickering in the bar. Notice what stayed as it was: the construction of the guard is identical to authGuard - the same type, the same inject(), the same createUrlTree() on refusal. One line is new, the one that reads the order. This time I did not spell out the second parameter, because I do not need the target address here, and TypeScript permits a shorter parameter list.Gates watch the way in, but there is an opposite situation in which you also need a guard. A student is filling in a long application to a clan, has three quarters of it entered, and then clicks "Armoury" in the navigation. Angular will do what was asked of it: destroy the form component and show the armoury. The work is gone, and the user finds out about it at the moment when it is already too late.
That is what the second kind of guard is for, triggered not on entry but on leaving a route. Its type is called
CanDeactivateFn and it differs from the one you already know in a single respect: its first parameter is the instance of the component that is about to disappear. Angular does not know in advance what component that will be, so the type is generic - in angle brackets you tell it what to expect. Read the notation CanDeactivateFn<TypeName> as "an exit guard for a component described by TypeName".We do not want to tie the guard to one class, because there will be many forms in the application. Instead we will describe a contract: any component that can answer the question about unsaved changes. TypeScript's tool for describing such contracts is
interface - a declaration stating what fields and methods an object must have, without saying how they are made.1// guards/unsaved-changes.guard.ts
2export interface CanComponentDeactivate {
3 hasUnsavedChanges: () => boolean;
4}This interface creates nothing that runs and vanishes without a trace after compilation - it exists purely so that TypeScript has something to enforce. Read it like this: a component fulfilling this contract has a
hasUnsavedChanges method that takes no arguments and returns a boolean. So the form component will add implements CanComponentDeactivate and implement that one method, and the guard will not have to know anything more about it.Now the guard itself. When the changes are unsaved, we want to ask the user rather than decide on their behalf. The simplest way to do that is
confirm() - the browser's built-in dialog with a question and two buttons, returning true after a confirmation and false after a cancellation. That is exactly the type the guard expects, so the result can be handed straight back.1// guards/unsaved-changes.guard.ts
2import { CanDeactivateFn } from '@angular/router';
3
4export const unsavedChangesGuard: CanDeactivateFn<CanComponentDeactivate> = (component) => {
5 if (component.hasUnsavedChanges()) {
6 return confirm('You have unsaved changes. Are you sure you want to leave this screen?');
7 }
8 return true;
9};The layout of the decision here is the reverse of the entrance gate, so read carefully: if there are no changes, the function reaches
return true and the exit happens with no dialog whatsoever - a user who touched nothing will not be asked about anything. We ask only when there is something to lose. True from confirm() means "let them out", false means "stay", and on false Angular aborts the navigation and the address in the bar returns to the form's route. Notice what the exit guard does not do: it saves nothing. This is not a mechanism that protects data, only a warning - saving still belongs to the component.One last step remains, the same one as at the gate: the guard has to reach the route entry. The field is called
canDeactivate, it is an array just like canActivate, and it likewise takes the bare name of the function, with no parentheses.1// app.routes.ts, a fragment of the routes array
2{
3 path: 'samurai/:id/edit',
4 component: SamuraiEditComponent,
5 canDeactivate: [unsavedChangesGuard]
6}From now on every departure from the editing screen - by a click in the navigation, by the browser's back button or by a
navigate() call from code - passes through that question. I checked both answers on a running router: after a confirmation the navigation goes through, and after a cancellation the address stays on the editing route while the form component lives on with all its contents. None of this changes anything in that route's canActivate field or in the entry guards - these are two independent posts, one at the way in, one at the way out.There are more guards in the router than two, and their roles are easy to mix up, because the names resemble one another. So let us line the whole watch up in one place, together with the question each of them answers.
| Guard | When it asks | The question it answers | |-------|--------------|-------------------------| | CanActivate | before entering a route | can this user enter here | | CanActivateChild | before entering a child route | can they go deeper, into the sub-routes | | CanDeactivate | on an attempt to leave a route | are they allowed out of here | | CanMatch | while the address is being matched | should this route entry be considered at all | | Resolve | after the guards, before activation | what data to fetch before the screen appears |
Exactly one of them answers the question "can this user enter this path", and it is
CanActivate - the rest are busy with something else. CanDeactivate watches the opposite direction, so on the way in it will not speak up at all. Resolve is not really a guard but a supplier: it cannot say "I am not letting you in", its job is to bring data, and you will get to know it properly in the next lesson. A separate word is due to CanLoad, which you will run into in older tutorials: it watched over the downloading of the bundle for a lazily loaded route, but it has been replaced by CanMatch, and in Angular 19 its field in the route entry is marked as deprecated. Neither of those two answers the question about entering an already matched route - they decide whether a given route comes into play at all.Fix one more thing in your head, the order in which all of this happens in the life of a single route, because without it a wrong assumption comes easily.
CanActivate speaks first and decides whether you get in at all. Only once it has let you through do the resolvers set off for data - and that order is the sensible one, because there is no point pulling supplies off the server for somebody we are about to turn back at the gate. Then the screen lives its own life, sometimes for a long time. And CanDeactivate speaks up only at the end, on an attempt to leave - when there is finally something worth protecting.Remember, @name: a gate without a guard is just a hole in the wall, and a guard has one job - to say yes or no, and to point the way to whoever he turned back.