Guardy kontrolują dostęp podczas gdy resolvery odpowiadają za pobieranie danych. Pomagają nam chronić wybrane przez nas ścieżki i wstępnie załadować niezbędne informacje. Mimo, że korzystamy z nich w większości naszych aplikacji, często nie rozumiemy jak faktycznie działają “behind-the-scenes”. Pisząc ten artykuł zdałem sobie sprawę, że nigdy dostatecznie nie pochyliłem się nad tematem a napisanie tego artykułu pomogło mi załatać własne braki.
Zacznijmy od guard’ów
Guard’y działają jak checkpointy, zarządzające tym czy mamy dostęp do określonych “dróg”. Wykonują określone akcje w zależności od tego czy dostęp zostanie przyznany lub nie.
Tworzenie guardów
Angular jak zwykle wychodzi naprzeciw naszym oczekiwaniom i ułatwia kwestie tworzenia guardów. Możemy to zrobić w prosty sposób z naszych CLI.
[terminal]
ng generate guard NAZWA_GUARDA
Po wykonaniu powyższej komendy, będziemy mieli możliwość wybrania typu guarda spośród czterech dostępnych:

Każdy z nich ma trochę inną funkcję, przejdziemy po kolei przez wszystkie. Należy dodać, że mamy także możliwość stworzenia każdego z tych guardów manualnie tworząc plik typescript. Standardową praktyką dla zachowania spójności powinno być dodanie suffixu -guard.ts tak by wyróżnić jego funkcję w naszych projektach.
Typy zwrotek z guardów
Każdy typ guardów zwraca ten sam typ. Dodaje nam to elastyczności w kwestii tego jak będziemy kontrolowali nawigację i gdzie przekierujemy użytkownika.
- Boolean – zezwala lub blokuje nawigację, sytuacja ma się trochę inaczej w guardzie CanMatch, który gdy dostaje wartość false sprawdza kolejne mogące pasować ścieżki zamiast blokować kompletnie nawigację.
- UrlTree lub RedirectComand – pozwala na przekierowanie do innej ścieżki zamiast blokowania.
- Promise lub Observable – wykorzystuje zwróconą wartość do kontynuowania lub anulowania nawigacji.
Rodzaje guardów
Zanim przejdziemy przez opisywanie każdego z nich, należy dodać, że guard’y mają dostęp do serwisów dostarczonych na poziomie routingu jak i innych informacji dostępnych przez argument route. Przykład prosto z dokumentacji pozwoli nam sobie to zobrazować:
export const routes: Routes = [
{
path: 'admin',
providers: [
AdminService, // Only loaded with admin routes
{ provide: FEATURE_FLAGS, useValue: { adminMode: true } },
],
loadChildren: () => import('./admin/admin.routes'),
},
{
path: 'shop',
providers: [
ShoppingCartService, // Isolated shopping state
PaymentService,
],
loadChildren: () => import('./shop/shop.routes'),
},
];
CanActivate
Przejdźmy do pierwszego guarda, którego najczęściej wykorzystujemy w naszych projektach. CanActivate sprawdza, czy użytkownik może uzyskać dostęp do wskazanej ścieżki. Najczęściej używamy go by rozwiązać kwestie odpowiedniego przekierowania użytkownika podczas uwierzytelniania i autoryzacji.
Guard ma dostęp do dwóch podstawowych argumentów:
- route: ActivatedRouteSnapshot – zawiera informacje o trasie, która ma zostać aktywowana

- state: RouterStateSnapshot – zawiera informacje o aktualnym stanie routera

Aby użytkownik mógł przejść dalej, guard musi zwrócić true. Jeśli zwróci false, nawigacja zostanie anulowana. Natomiast gdy guard zwróci UrlTree, bieżąca nawigacja zostaje przerwana, a router rozpoczyna nową nawigację na podstawie zwróconego UrlTree.
Krótki snippet kodu pomagający zrozumieć działanie:
export const authGuard: CanActivateFn = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
) => {
const authService = inject(AuthService);
return authService.isAuthenticated();
};
CanActivateFn to sygnatura funkcji używanej jako guard canActivate w konfiguracji Route.
Przykład
Przed przejściem do kolejnego guard’a chciałbym pokazać Wam kod, który sam bardzo często wykorzystuję. Guard ten pozwala chronić ścieżki przed nieautoryzowanym wejściem. Uniemożliwia także przejście do ścieżek takich jak np. strona logowania po pomyślnym zalogowaniu.
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth-service';
export const createAuthGuard = (requiresAuth: boolean, redirectUrl: string): CanActivateFn => {
return () => {
const router = inject(Router);
const authService = inject(AuthService);
if (authService.isLoggedIn() === requiresAuth) {
return true;
}
return router.createUrlTree([redirectUrl]);
};
};
export const requireAuth = (redirectUrl = '/login') => createAuthGuard(true, redirectUrl);
export const requireNoAuth = (redirectUrl = '/dashboard') => createAuthGuard(false, redirectUrl);
app.routes.ts
export const routes: Routes = [
{
path: 'login',
canActivate: [requireNoAuth()],
loadComponent: () => import('./pages/login/login').then((c) => c.Login),
},
{
path: 'dashboard',
canActivate: [requireAuth()],
loadComponent: () => import('./pages/dashboard/dashboard').then((c) => c.Dashboard),
},
{
path: '',
redirectTo: '/login',
pathMatch: 'full',
},
];
Piękny, prosty i przejrzysty, prawda? Przejdźmy jednak dalej.
CanActivateChild
Drugi guard określa, czy użytkownik może uzyskać dostęp do tras potomnych (child routes) wybranej ścieżki nadrzędnej. Jest to szczególnie przydatne, gdy chcemy zabezpieczyć całą sekcję zagnieżdżonych tras. Guard uruchamiany jest dla wszystkich dzieci danej trasy — również wtedy, gdy występują kolejne poziomy zagnieżdżenia.
Ten guard ma dostęp do dwóch argumentów:
- childRoute: ActivatedRouteSnapshot – zawiera informacje o „przyszłym” stanie aktywowanej trasy potomnej (czyli tej, do której router próbuje przejść),
- state: RouterStateSnapshot – zawiera informacje o docelowym stanie routera.
Podobnie jak w przypadku CanActivate, guard może: zwrócić true (nawigacja jest kontynuowana), zwrócić false (nawigacja zostaje anulowana), zwrócić UrlTree (następuje przekierowanie).
Przykład kodu z wykorzystaniem CanActivateChild:
export const adminChildGuard: CanActivateChildFn = (
childRoute: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
) => {
const authService = inject(AuthService);
return authService.hasRole('admin');
};
CanActivateChildFn to sygnatura funkcji używanej jako guard canActivateChild w konfiguracji tras (Route).
CanDeactivate
Guard CanDeactivate wyróżnia się tym, że — w przeciwieństwie do pozostałych — kontroluje możliwość opuszczenia aktualnej trasy. Najczęściej używany do zapobiegania utracie danych, np. gdy użytkownik próbuje opuścić stronę z niezapisanym formularzem.
Podstawowe argumenty, które otrzymuje ten guard:
- component: T – instancja komponentu, który jest dezaktywowany
- currentRoute: ActivatedRouteSnapshot – zawiera informacje o aktualnej trasie
- currentState: RouterStateSnapshot – zawiera informacje o bieżącym stanie routera
- nextState: RouterStateSnapshot – zawiera informacje o docelowym stanie routera, do którego następuje nawigacja
Guard działa podobnie jak pozostałe i może: zwrócić true (nawigacja jest kontynuowana), zwrócić false (nawigacja zostaje anulowana), zwrócić UrlTree (następuje przekierowanie). Snippet kodu z dokumentacji:
export const unsavedChangesGuard: CanDeactivateFn<Form> = (
component: Form,
currentRoute: ActivatedRouteSnapshot,
currentState: RouterStateSnapshot,
nextState: RouterStateSnapshot,
) => {
return component.hasUnsavedChanges()
? confirm('You have unsaved changes. Are you sure you want to leave?')
: true;
};
CanDeactivateFn to sygnatura funkcji używanej jako guard canDeactivate w konfiguracji tras (Route).
CanMatch
To ostatni guard, który decyduje, czy dana trasa powinna zostać dopasowana (matchowana) podczas procesu routingu. Wyróżnia się tym, że jeśli guard zwróci false, Angular nie blokuje nawigacji, tylko próbuje dopasować kolejne dostępne trasy. Dzięki temu świetnie sprawdza się w scenariuszach takich jak: feature flags, testy A/B, warunkowe ładowanie tras.
Argumenty, które otrzymuje guard:
- route: Route – konfiguracja ocenianej trasy
- segments: UrlSegment[] – segmenty URL, które nie zostały jeszcze dopasowane przez wcześniejsze poziomy routingu
Guard zwraca standardowe typy (true, false, UrlTree), jednak w przypadku false Angular próbuje dopasować kolejne trasy zamiast przerywać nawigację.
Proste przykłady kodu:
export const featureToggleGuard: CanMatchFn = (route: Route, segments: UrlSegment[]) => {
const featureService = inject(FeatureService);
return featureService.isFeatureEnabled('newDashboard');
};
Można go również wykorzystać do warunkowego przypisania komponentu dla tej samej ścieżki:
const routes: Routes = [
{
path: 'dashboard',
component: AdminDashboard,
canMatch: [adminGuard],
},
{
path: 'dashboard',
component: UserDashboard,
canMatch: [userGuard],
},
];
CanMatchFn to sygnatura funkcji używanej jako guard canMatch w konfiguracji tras (Route).
Poprzednik CanMatch czyli CanLoad (deprecated)
Mimo, że CanLoad został zastąpiony CanMatch możemy dalej spotkać go w starszych projektach. Guard ten służył do sprawdzania czy children może zostać załadowany. Jeżeli wszystkie guard’y zwrócą true, nawigacja jest kontynuowana. Jeżeli jeden z nich zwróci false, nawigacja jest anulowana. Jest jeszcze opcja, w której jeden z guard’ów zwraca UrlTree, wtedy bieżąca nawigacja jest przerywana a router rozpoczyna nawigację na podstawie podanego drzewa.
Używanie guardów w konfiguracji routes’ów
Gdy już utworzyłeś guardy, chcesz je skonfigurować w definicjach routes’ów. Są one używane w tablicach by móc korzystać z kilku guardów do pojedynczej ścieżki. Uruchamiamy je zgodnie z kolejnością z array’a. A co gdy dwa elementy tablicy zwracają wartość true? Oczywiście tutaj też kolejność jest brana pod uwagę i przejdziemy do pierwszego path’a.
const routes: Routes = [
// Basic CanActivate - requires authentication
{
path: 'dashboard',
component: Dashboard,
canActivate: [authGuard],
},
// Multiple CanActivate guards - requires authentication AND admin role
{
path: 'admin',
component: Admin,
canActivate: [authGuard, adminGuard],
},
// CanActivate + CanDeactivate - protected route with unsaved changes check
{
path: 'profile',
component: Profile,
canActivate: [authGuard],
canDeactivate: [canDeactivateGuard],
},
// CanActivateChild - protects all child routes
{
path: 'users', // /user - NOT protected
canActivateChild: [authGuard],
children: [
// /users/list - PROTECTED
{path: 'list', component: UserList},
// /users/detail/:id - PROTECTED
{path: 'detail/:id', component: UserDetail},
],
},
// CanMatch - conditionally matches route based on feature flag
{
path: 'beta-feature',
component: BetaFeature,
canMatch: [featureToggleGuard],
},
// Fallback route if beta feature is disabled
{
path: 'beta-feature',
component: ComingSoon,
},
];
Resolvery danych
Resolvery pozwalają na załadowanie danych przed nawigowaniem do ścieżki. Jesteśmy wtedy pewni, że komponenty otrzymają dane zanim się wyrenderują. Możemy zrezygnować ze stanów ładowania co daje nam możliwość poprawienia ogólnego UX przez pre-loading najpotrzebniejszych danych. Wiąże się to jednak z tym, że możemy dłużej czekać na koniec redirect’u. Tak więc jedno i drugie podejście ma swoje plusy i minusy, musisz sam wybrać co jest lepsze dla Twojej aplikacji.
Czym są i po co ich używać?
Resolvery to funkcje (ResolveFn) lub klasy odpowiedzialne za pobranie danych przed aktywacją trasy. Są uruchamiane przed tym, jak wskazana ścieżka się uruchomi, a to pozwala na wczytanie danych w komponencie przez ActivatedRoute. Resolvery mają dostęp do serwisów, dostarczonych na route level’u, a także do informacji samej ścieżki przekazywanych w argumencie route. To wszystko pomaga nam zażegnać kilka częstych kłopotów jak:
- puste stany – komponenty nie muszą czekać na dane, bo mają je w momencie załadowania widoku
- lepszy ux – nie ma ciągłego ładowania (spinnerów/skeletonów itp.) w oczekiwaniu na kluczowe informacje
- skuteczna obsługa błędów – problemy pobierania danych są rozwiązywane przed zmianą adresu URL, przez co możemy kontrolować zachowanie użytkownika na naszej stronie
- spójność danych – gwarancja, że dane są gotowe przed renderowaniem, krytyczne w przypadku SSR, gdzie serwer musi wysłać gotową stronę do przeglądarki
Tworzenie resolverów
Tworzenie resolverów to prosta operacja utworzenia funkcji z odpowiednim typem ResolveFn. Funkcja ta otrzymuje ActivatedRouteSnapshot oraz RouterStateSnapshot jako parametry. Poniżej przykład resolverów z dokumentacji, który pobiera informacje o użytkowniku przed wyrenderowaniem ścieżki:
import {inject} from '@angular/core';
import {UserStore, SettingsStore} from './user-store';
import type {ActivatedRouteSnapshot, ResolveFn, RouterStateSnapshot} from '@angular/router';
import type {User, Settings} from './types';
export const userResolver: ResolveFn<User> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
) => {
const userStore = inject(UserStore);
const userId = route.paramMap.get('id')!;
return userStore.getUser(userId);
};
export const settingsResolver: ResolveFn<Settings> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
) => {
const settingsStore = inject(SettingsStore);
const userId = route.paramMap.get('id')!;
return settingsStore.getUserSettings(userId);
};
Konfigurowanie routes z resolverami
Możesz dodać wiele resolverów do Twoich ścieżek jako key, value pairs w konfiguracji Routes, wygląda to mniej więcej tak:
import {Routes} from '@angular/router';
export const routes: Routes = [
{
path: 'user/:id',
component: UserDetail,
resolve: {
user: userResolver,
settings: settingsResolver,
},
},
];
Resolve to mapa kluczy danych przypisanych do resolverów, których wyniki będą dostępne w ActivatedRoute.data.
Pobieranie dostępnych danych z resolverów w komponentach
ActivatedRoute
Pierwszym sposobem aby otrzymać dane w komponencie jest snapshot danych z ActivatedRoute używając sygnałów:
@Component({
template: `
<!-- We call user() and settings() as functions because they are Signals -->
<h1>{{ user().name }}</h1>
<p>{{ user().email }}</p>
<div>Theme: {{ settings().theme }}</div>
`,
})
export class UserDetail {
// Inject the current route information
private route = inject(ActivatedRoute);
/**
* toSignal converts the route.data Observable into a reactive Signal.
* This Signal will update whenever the route parameters or data change.
*/
private data = toSignal(this.route.data, { requireSync: true });
/**
* computed() creates a derived Signal.
* It automatically recalculates whenever 'data' changes.
* We cast 'as User' because route data is typed as 'any' by default.
*/
user = computed(() => this.data().user as User);
/**
* Derived signal for settings.
* This keeps the template clean and provides type safety.
*/
settings = computed(() => this.data().settings as Settings);
}
withComponentInputBinding
Kolejnym sposobem jest konfiguracja withComponentInputBinding() w ramach provideRouter. Pozwala to na odbieranie danych bezpośrednio przez input’y w komponencie, co jest zdecydowanie bardziej eleganckie niż korzystanie z ActivatedRoute. Mechanizm ten może również wiązać informację z:
- query parameters
- path i matrix parameters
- static route data
- data z resolver’ów
import {bootstrapApplication} from '@angular/platform-browser';
import {provideRouter, withComponentInputBinding} from '@angular/router';
import {routes} from './app.routes';
bootstrapApplication(App, {
providers: [provideRouter(routes, withComponentInputBinding())],
});
Odbieranie danych poprzez input, które zapewnia lepsze zabezpieczenie typów oraz eliminuje potrzebę injectowania ActivatedRoute:
import {Component, input} from '@angular/core';
import type {User, Settings} from './types';
@Component({
template: `
<h1>{{ user().name }}</h1>
<p>{{ user().email }}</p>
<div>Theme: {{ settings().theme }}</div>
`,
})
export class UserDetail {
user = input.required<User>();
settings = input<Settings>();
}
Obsługa błędów w resolvers
Chcąc zachować dobry user experience musimy obsłużyć błędy w naszych resolverach. Nie chcemy pozostawić defaultowego zachowania, które wywoła NavigationError skutecznie psując doświadczenia na naszej stronie. Mamy na to trzy podstawowe sposoby.
Scentralizowane obsługiwanie błędów- withNavigationErrorHandler
Ta klauzula pozwala na wprowadzenie scentralizowanego sposobu obsługi wszystkich błędów nawigacyjnych w tym tych, które zostały zwrócone przez data resolvers. To pozwala trzymać logikę w jednym miejscu co ogranicza duplikowanie kodu pomiędzy resolverami. Spójrz na poniższy przykład z dokumentacji:
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter, withNavigationErrorHandler } from '@angular/router';
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { routes } from './app.routes';
bootstrapApplication(App, {
providers: [
provideRouter(
routes,
withNavigationErrorHandler((error) => {
const router = inject(Router);
if (error?.message) {
console.error('Navigation error occurred:', error.message);
}
router.navigate(['/error']);
}),
),
],
});
Taka konfiguracja pozwala resolverom na skupienie się na jednym task’u na raz np. fetchowaniu danych.
export const userResolver: ResolveFn<User> = (route) => {
const userStore = inject(UserStore);
const userId = route.paramMap.get('id')!;
// No need for explicit error handling - let it bubble up
return userStore.getUser(userId);
};
Obsługa błędów poprzez subskrypcje router eventów
To drugi sposób obsługi błędów w resolverach. Subskrybujemy do router events a potem nasłuchujemy eventów NavigationError. Takie podejście zapewnia większą kontrolę i umożliwia wdrożenie niestandardowej logiki odzyskiwania po błędach.
import { Component, inject, signal } from '@angular/core';
import { Router, NavigationError } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs';
@Component({
selector: 'app-root',
template: `
@if (errorMessage()) {
<div class="error-banner">
{{ errorMessage() }}
<button (click)="retryNavigation()">Retry</button>
</div>
}
<router-outlet />
`,
})
export class App {
private router = inject(Router);
private lastFailedUrl = signal('');
private navigationErrors = toSignal(
this.router.events.pipe(
map((event) => {
if (event instanceof NavigationError) {
this.lastFailedUrl.set(event.url);
if (event.error) {
console.error('Navigation error', event.error);
}
return 'Navigation failed. Please try again.';
}
return '';
}),
),
{ initialValue: '' },
);
errorMessage = this.navigationErrors;
retryNavigation() {
if (this.lastFailedUrl()) {
this.router.navigateByUrl(this.lastFailedUrl());
}
}
}
Obsługa bezpośrednio w resolverze
To nic innego jak przechwytywanie błędu bezpośrednio w resolverze/funkcji wykorzystującej ResolveFn. Spójrz na zmodyfikowany przykład, który już wcześniej używaliśmy.
import { inject } from '@angular/core';
import { ResolveFn, RedirectCommand, Router } from '@angular/router';
import { catchError, of } from 'rxjs';
import { UserStore } from './user-store';
import type { User } from './types';
export const userResolver: ResolveFn<User | RedirectCommand> = (route) => {
// Inject dependencies using the functional inject() API
const userStore = inject(UserStore);
const router = inject(Router);
// Extract the 'id' parameter from the route URL (e.g., /users/:id)
const userId = route.paramMap.get('id')!;
// Fetch user data from the store and handle potential errors
return userStore.getUser(userId).pipe(
catchError((error) => {
// Log the error for debugging purposes
console.error('Failed to load user:', error);
/**
* If fetching fails (e.g., 404), return a RedirectCommand.
* This stops the current navigation and redirects the user to the list page.
*/
return of(new RedirectCommand(router.parseUrl('/users')));
}),
);
};
Dodatkowe informacje o resolverach
Loading state
Choć resolvery zapobiegają wyświetlaniu stanu ładowania wewnątrz samych komponentów, nie powstrzymują zablokowania nawigacji podczas jej wykonywania. Rodzi to problem, w którym user może odczuwać opóźnienie między kliknięciem w link, a faktyczną zmianą widoku. Chcąc poprawić UX podczas działania resolverów, możesz nasłuchiwać wydarzeń i pokazywać stan ładowania:
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
@Component({
selector: 'app-root',
template: `
@if (isNavigating()) {
<div class="loading-bar">Loading...</div>
}
<router-outlet />
`,
})
export class App {
private router = inject(Router);
isNavigating = computed(() => !!this.router.currentNavigation()); //to nowe podejście będącę następcą NavigationStart/End
}
Ładowanie danych rodzica do child resolver’a
Resolvery wykonują się w kolejności od rodzica do dziecka (parent-to-child). Gdy trasa nadrzędna ze zdefiniowanym resolverem pobierze dane, są one dostępne dla resolverów tras podrzędnych uruchamianych później w kolejności.
import { inject } from '@angular/core';
import { provideRouter, ActivatedRouteSnapshot } from '@angular/router';
import { userResolver } from './resolvers';
import { UserPosts } from './pages';
import { PostService } from './services';
import type { User } from './types';
provideRouter([
{
path: 'users/:id',
resolve: { user: userResolver }, // user resolver in the parent route
children: [
{
path: 'posts',
component: UserPosts,
// route.data.user is available here while this resolver runs
resolve: {
posts: (route: ActivatedRouteSnapshot) => {
const postService = inject(PostService);
const user = route.parent?.data['user'] as User; // parent data
const userId = user.id;
return postService.getPostByUser(userId);
},
},
},
],
},
]);
Podsumowanie
Poruszone dziś tematy są bardzo ważne, jeżeli chcemy pisać aplikacje bezpieczne oraz przyjazne użytkownikom. Nieważne czy zaczynasz swoją przygodę czy jesteś już Angularowym wyjadaczem, przedstawione dziś rozwiązania są używane w większości pisanych przez nas aplikacji. Tak więc dogłębne ich zrozumienie jest kluczowe do pisania bezpiecznych i funkcjonalnych aplikacji.