install
source · Clone the upstream repo
git clone https://github.com/Intense-Visions/harness-engineering
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/claude-code/angular-routing-guards" ~/.claude/skills/intense-visions-harness-engineering-angular-routing-guards && rm -rf "$T"
manifest:
agents/skills/claude-code/angular-routing-guards/SKILL.mdsource content
Angular Routing Guards
Protect and preload routes with functional CanActivateFn, CanDeactivateFn, ResolveFn, and CanMatchFn guards
When to Use
- Redirecting unauthenticated users away from protected routes (
)CanActivateFn - Warning users about unsaved changes before navigating away (
)CanDeactivateFn - Pre-fetching data before a route renders to avoid loading spinners in the component (
)ResolveFn - Conditionally loading a lazy module based on feature flags or roles (
)CanMatchFn - Composing multiple guard conditions with
or short-circuit logiccombineLatest
Instructions
- Write guards as plain functions (functional guard pattern, Angular 14.2+), not class-based
implementations. Functional guards useCanActivate
directly and are easier to test.inject() - Return
,true
,false
,UrlTree
, orObservable<boolean | UrlTree>
from a guard. Return aPromise<boolean | UrlTree>
(viaUrlTree
) to redirect instead of just blocking.inject(Router).createUrlTree(['/login']) - Prefer
for authentication checks. Redirect to the login page and pass the attempted URL as a query param so the login page can redirect back after success.CanActivateFn - Use
to load required data before the route activates. The resolved data is available inResolveFn
. This eliminates the need for loading states in the component.ActivatedRoute.data - Use
to warn users about unsaved changes. The guard receives the component instance — define an interface the component implements (CanDeactivateFn
) and check it in the guard.HasUnsavedChanges - Use
instead ofCanMatchFn
when you want to prevent a lazy module from even loading (not just block navigation to it). This saves bundle bytes for unauthorized users.CanActivateFn - Compose guards in the route's
array — all must returncanActivate
for the route to activate.true
// auth.guard.ts — functional authentication guard import { inject } from '@angular/core'; import { CanActivateFn, Router } from '@angular/router'; import { AuthService } from './auth.service'; import { map } from 'rxjs'; export const authGuard: CanActivateFn = (route, state) => { const auth = inject(AuthService); const router = inject(Router); return auth.isAuthenticated$.pipe( map((isAuth) => isAuth ? true : router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url }, }) ) ); }; // role.guard.ts — role-based access export const adminGuard: CanActivateFn = () => { const auth = inject(AuthService); const router = inject(Router); return auth.hasRole('admin') ? true : router.createUrlTree(['/forbidden']); };
// unsaved-changes.guard.ts — CanDeactivateFn export interface HasUnsavedChanges { hasUnsavedChanges(): boolean; } export const unsavedChangesGuard: CanDeactivateFn<HasUnsavedChanges> = (component) => { if (component.hasUnsavedChanges()) { return confirm('You have unsaved changes. Leave anyway?'); } return true; };
// product.resolver.ts — ResolveFn import { ResolveFn } from '@angular/router'; import { inject } from '@angular/core'; import { ProductService } from './product.service'; import { Product } from './product.model'; export const productResolver: ResolveFn<Product> = (route) => { return inject(ProductService).getById(route.paramMap.get('id')!); }; // Route config { path: 'product/:id', component: ProductDetailComponent, resolve: { product: productResolver }, canActivate: [authGuard], } // Component reads resolved data export class ProductDetailComponent { product = inject(ActivatedRoute).snapshot.data['product'] as Product; }
Details
Functional vs class guards: Class-based guards implementing
CanActivate interface are deprecated in Angular 15+. Functional guards have no class overhead, use inject() directly, and are composable as arrays in route config. If you need to wrap a class-based guard for migration, use mapToCanActivate([LegacyGuard]) as a bridge.
vs CanMatchFn
: CanActivateFn
CanActivate runs after the route is matched but before it renders. CanMatch runs during route matching — if it returns false, Angular continues trying other route alternatives. This means CanMatch can prevent lazy chunks from loading entirely, reducing bandwidth for unauthorized users. It also enables showing different components for the same URL path based on conditions (e.g., A/B testing).
Resolver error handling: If a
ResolveFn throws or the observable errors, Angular cancels navigation by default. Add error handling in the resolver or use a catchError to return a fallback:
export const productResolver: ResolveFn<Product | null> = (route) => { return inject(ProductService) .getById(route.paramMap.get('id')!) .pipe( catchError(() => { inject(Router).navigate(['/not-found']); return of(null); }) ); };
Guard composition: Angular runs
canActivate guards in the array order, but all run in parallel by default if they return observables. For serial execution (first guard must pass before second runs), compose with switchMap:
export const composedGuard: CanActivateFn = (route, state) => authGuard(route, state).pipe( switchMap((authed) => (authed === true ? adminGuard(route, state) : of(authed))) );
Testing functional guards:
TestBed.configureTestingModule({ providers: [{ provide: AuthService, useValue: mockAuthService }, provideRouter([])], }); const result = TestBed.runInInjectionContext(() => authGuard(mockRoute, mockState));
Source
https://angular.dev/guide/routing/common-router-tasks
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.