Guards: CanActivate, Reflector Metadata, and Role-Based Authorization
- Last updated
- Prerequisites:
- NestJS ExecutionContext
- TypeScript Decorators & Metadata
- nestjs
- guards
- canActivate
- reflector
- authorization
- rbac
Read at your depth
The practical view
Guards determine whether a request should be handled by a route handler. They implement `CanActivate` with a `canActivate(context: ExecutionContext): boolean | Promise<boolean>`. If it returns `true`, the request proceeds; if `false`, NestJS returns `403 Forbidden`. Use `@UseGuards(AuthGuard)` at the controller or method level, or register globally with `APP_GUARD`.
The same idea in other frameworks
`@UseGuards(RolesGuard)` implementing `CanActivate` with `Reflector`.Spring Security `@PreAuthorize("hasRole('ADMIN')")` expression evaluator.FastAPI `Security(get_current_user, scopes=["admin"])` dependencies.Custom `(req, res, next)` authentication middleware functions.Legacy vs modern
Middleware Route Protection vs Declarative NestJS RBAC Guard
Express middleware requires hardcoded URL regex matching; NestJS Guards use declarative metadata decorators.
if (req.path.startsWith('/admin') && req.user.role !== 'admin') res.status(403).send(); // ❌ Brittle regex routes@Roles(Role.Admin) @UseGuards(RolesGuard) // ✅ NEW: Declarative, type-safe metadataInterview gotchas
Context
This is one of the most frequently asked NestJS architectural lifecycle questions.
The mechanical answer
The exact incoming request execution order is: 1. **Global / Module Middleware** 2. **Guards** (Global -> Controller -> Route) 3. **Interceptors (Pre-controller logic)** 4. **Pipes** (Global -> Controller -> Route -> Parameter) 5. **Controller Route Handler** 6. **Interceptors (Post-controller RxJS response mapping)** 7. **Exception Filters** (if an error is thrown anywhere)
Trap
Guards run *before* Pipes. This means parameter validation and type transformation have not occurred yet when a Guard executes.
Context
Checks understanding of multi-provider tokens vs `app.useGlobalGuards()`.
The mechanical answer
Do not instantiate it in `main.ts` with `new Guard()`. Instead, bind it in your `AppModule` using the **`APP_GUARD`** token: ```typescript { provide: APP_GUARD, useClass: RolesGuard } ``` This ensures the guard is instantiated within the NestJS IoC container with full dependency injection support.
Trap
Using `app.useGlobalGuards(new RolesGuard())` in `main.ts` prevents dependency injection because the instance is created outside the NestJS container.