Lesson 18 of 30
A guard decides whether a navigation is even allowed to happen; a resolver decides what data the component gets before it renders. Both are just functions now.
15 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Two questions come up constantly when you're routing: "is this user even allowed to see this page?" and "does this page need data before it renders, so it doesn't flash empty and then pop in content?"
A guard answers the first question — it runs before a navigation completes and can allow it, block it, or redirect elsewhere. A resolver answers the second — it runs before a navigation completes and fetches data the component needs, so the component only activates once that data already exists.
Older Angular did both as classes implementing an interface (implements CanActivate). Current Angular writes both as plain functions — CanActivateFn and ResolveFn — that call inject() to reach whatever services they need. No class, no constructor, no module wiring.
// auth.guard.ts
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;
}
// Redirect instead of a bare `false` — this is how you send the user
// somewhere useful instead of just silently refusing the navigation.
return router.createUrlTree(['/login'], { queryParams: { redirectTo: state.url } });
};
// user.resolver.ts
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { UserService } from './user.service';
import { User } from './user.model';
export const userResolver: ResolveFn<User> = (route) => {
const users = inject(UserService);
const id = route.paramMap.get('id')!;
return users.getById(id); // an Observable<User> — the router waits for it to emit
};
Both slot into the route config as plain array entries:
// app.routes.ts
export const routes: Routes = [
{
path: 'account/:id',
component: AccountComponent,
canActivate: [authGuard],
resolve: { user: userResolver },
},
];
And because withComponentInputBinding() binds resolved data by name too (not just route params), AccountComponent can pick the resolved user straight off a signal input:
@Component({ selector: 'app-account', standalone: true, template: `<h1>{{ user().name }}</h1>` })
export class AccountComponent {
user = input.required<User>(); // populated by the 'user' key from resolve: {...}
}
inject() there is running in the router's injection context, not the routed component's.userResolver's Observable never completes, the navigation just hangs — no route change, no error shown, nothing. Resolvers should generally have their own error/timeout handling rather than trusting the network to always answer.implements CanActivate with a canActivate() method, that's the exact same feature — just the older syntax. The route config API (canActivate: [...]) accepts both, so a codebase can have a mix during migration.inject() doing guard and resolver dutyBoth authGuard and userResolver lean entirely on inject() outside a constructor — the same dependency-injection mechanism from the DI fundamentals lesson, just called from a plain function instead of a class body. It's the same trick TypeScript decorator-averse codebases use everywhere functional patterns replaced class-based ones: keep the DI container, drop the ceremony around it.