Guards and Authentication

A guard is a class with one method, canActivate, that returns true to let the request through or throws to refuse it. Guards run after middleware and before pipes, which is the order Endpoints argued for: an anonymous POST /books is refused before any schema work happens.

src/common/guards.ts — authenticate, then authorizeTypeScript
@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(private readonly jwt: JwtService) {}
  canActivate(ctx: ExecutionContext): boolean {
    const req = ctx.switchToHttp().getRequest();
    const [scheme, token] = (req.headers.authorization ?? '').split(' ');
    if (scheme !== 'Bearer' || !token)
      throw new UnauthorizedException('Authentication required');
    try {                                      // a broken token is 401, not anonymous
      const c = this.jwt.verify(token);
      req.user = { id: c.sub, roles: c.roles ?? [] };   // roles come from the signature
      return true;                             // never from a header a client controls
    } catch (e) { throw new UnauthorizedException((e as Error).message); } } }
@Injectable()
export class RolesGuard implements CanActivate {          // runs second, on req.user
  constructor(private readonly reflector: Reflector) {}
  canActivate(ctx: ExecutionContext): boolean {
    const need = this.reflector.getAllAndOverride<string[]>(ROLES_KEY,
      [ctx.getHandler(), ctx.getClass()]);   // method decorator beats class decorator
    const user = ctx.switchToHttp().getRequest().user;
    if (!need?.length || need.some((r) => user?.roles?.includes(r))) return true;
    throw new ForbiddenException(`Requires one of: ${need.join(', ')}`); } }

Reflector reads metadata a decorator attached to the route: @Roles('editor') is one line, SetMetadata(ROLES_KEY, roles). The order in @UseGuards(JwtAuthGuard, RolesGuard) is the order they run, so a missing token is 401 and a valid token without the role is 403.

Output of 110
$ curl -s -X POST localhost:4310/api/v1/books -d '{}'            # no token at all: 401
{"error":{"code":"not_authenticated","message":"Authentication required"},...}
$ curl -s -X POST .../books -H "auth: Bearer $READER" -d '{"title":"X",...}'   # 403
{"error":{"code":"forbidden","message":"Requires one of: editor"},"requestId":"22cb.."}

The 403 body proves the ordering: the validation pipe never ran, so the body was never parsed. Signing is configured once, in AuthModule, with JwtModule.registerAsync({ inject: [ConfigService], useFactory }) returning { secret: config.getOrThrow('ACCESS_SECRET'), signOptions: { expiresIn: '15m' } }.