Guards

Guards are a powerful feature that determines whether a request should be handled by the route handler or not. They are primarily used for authentication and authorization logic.

Use Cases

Guards are commonly used for:

  • Authentication: Verify user identity
  • Authorization: Check user permissions and roles
  • Rate limiting: Limit request frequency
  • Feature flags: Enable/disable features conditionally
  • Tenant isolation: Multi-tenant access control

Creating a Guard

A guard is a class that implements the CanActivate interface:

import { CanActivate, ExecutionContext, Injectable } from '@hazeljs/core';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean | Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    
    // Validate request
    return this.validateRequest(request);
  }

  private validateRequest(request: any): boolean {
    // Your validation logic here
    return true;
  }
}

The canActivate() method should return:

  • true - Allow the request to proceed
  • false - Deny the request (returns 403 Forbidden)
  • Promise<boolean> - For async validation

Using Guards

Method-scoped Guards

Apply to a single route handler:

import { Controller, Get, UseGuards } from '@hazeljs/core';
import { AuthGuard } from './guards/auth.guard';

@Controller('users')
export class UsersController {
  @Get('profile')
  @UseGuards(AuthGuard)
  getProfile() {
    return { message: 'This is a protected route' };
  }
}

Controller-scoped Guards

Apply to all routes in a controller:

@Controller('admin')
@UseGuards(AuthGuard, AdminGuard)
export class AdminController {
  // All routes require authentication and admin role
}

Global Guards

Apply to all routes in your application:

import { HazelApp } from '@hazeljs/core';
import { AuthGuard } from './guards/auth.guard';

const app = await HazelApp.create(AppModule);

app.useGlobalGuards(new AuthGuard());

await app.listen(3000);

Public routes: @Public() and @SkipAuth()

When using a global auth guard, some routes (e.g. login, health, webhooks) must remain public. Mark them with @Public() or @SkipAuth() so guards can skip authentication for those routes:

import { Controller, Get, Post, Body, UseGuards } from '@hazeljs/core';
import { JwtAuthGuard, CurrentUser } from '@hazeljs/auth';

@Controller('auth')
@UseGuards(JwtAuthGuard)  // applied to all routes by default
export class AuthController {
  @Public()
  @Get('health')
  health() {
    return { ok: true };
  }

  @Public()
  @Post('login')
  login(@Body() dto: LoginDto) {
    return this.authService.login(dto);
  }

  @Get('me')
  me(@CurrentUser() user: AuthUser) {
    return user;  // requires auth
  }
}

Guards (including JwtAuthGuard from @hazeljs/auth) should check Reflect.getMetadata('hazel:public', target, propertyKey) for the method or Reflect.getMetadata('hazel:public', target) for the class and allow the request when the value is true.

Authentication Guard

Here's a complete authentication guard using JWT:

import { 
  CanActivate, 
  ExecutionContext, 
  Injectable,
  UnauthorizedError,
} from '@hazeljs/core';
import * as jwt from 'jsonwebtoken';

@Injectable()
export class JwtAuthGuard implements CanActivate {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const authHeader = request.headers.authorization;

    if (!authHeader) {
      throw new UnauthorizedError('No authorization header');
    }

    const token = authHeader.replace('Bearer ', '');

    try {
      const payload = jwt.verify(token, process.env.JWT_SECRET || 'secret');
      
      // Attach user to request for use in handlers
      request.user = payload;
      
      return true;
    } catch (error) {
      throw new UnauthorizedError('Invalid or expired token');
    }
  }
}

Usage:

@Controller('users')
export class UsersController {
  @Get('profile')
  @UseGuards(JwtAuthGuard)
  getProfile(@Req() req: Request) {
    // req.user is available here
    return { user: req.user };
  }
}

Role-based Authorization Guard

@hazeljs/auth ships RoleGuard — a factory that returns a ready-made CanActivate class. No metadata decorators or Reflect calls needed.

import { Controller, Get, Delete, Param, UseGuards } from '@hazeljs/core';
import { JwtAuthGuard, RoleGuard } from '@hazeljs/auth';

// JwtAuthGuard must run first — it populates req.user
@UseGuards(JwtAuthGuard)
@Controller('/admin')
export class AdminController {
  @UseGuards(RoleGuard('admin'))     // admin and superadmin pass; manager/user → 403
  @Get('/users')
  getAllUsers() {
    return this.usersService.findAll();
  }

  @UseGuards(RoleGuard('superadmin')) // only superadmin
  @Delete('/users/:id')
  deleteUser(@Param('id') id: string) {
    return this.usersService.remove(id);
  }
}

Default role hierarchy

RoleGuard uses a built-in hierarchy so higher roles inherit all lower-role permissions:

superadmin
  └── admin
        └── manager
              └── user

RoleGuard('manager') passes for manager, admin, and superadmin — you only specify the minimum required role.

Custom hierarchy

Pass a plain object or a RoleHierarchy instance as the second argument:

import { RoleGuard, RoleHierarchy } from '@hazeljs/auth';

// Plain object shorthand
@UseGuards(RoleGuard('editor', { hierarchy: { owner: ['editor'], editor: ['viewer'], viewer: [] } }))

// Reusable named hierarchy
export const ContentHierarchy = new RoleHierarchy({
  owner:  ['editor', 'viewer'],
  editor: ['viewer'],
  viewer: [],
});

@UseGuards(JwtAuthGuard, RoleGuard('viewer', { hierarchy: ContentHierarchy }))
@Get('/posts')
listPosts() { ... }

Permission-based Guard

Check specific permissions instead of roles:

import { 
  CanActivate, 
  ExecutionContext, 
  Injectable,
  ForbiddenError,
  SetMetadata,
  getMetadata,
} from '@hazeljs/core';

export const RequirePermissions = (...permissions: string[]) => 
  SetMetadata('permissions', permissions);

@Injectable()
export class PermissionsGuard implements CanActivate {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    // If your context exposes the handler (or class + method name), use getMetadata('permissions', target, propertyKey).
    // For class-level only: getMetadata('permissions', ControllerClass).
    const handler = (context as { getHandler?: () => object }).getHandler?.();
    const requiredPermissions = handler
      ? getMetadata<string[]>('permissions', handler) || []
      : [];
    
    if (requiredPermissions.length === 0) {
      return true;
    }

    const user = request.user;
    
    if (!user) {
      throw new ForbiddenError('User not authenticated');
    }

    // Check if user has all required permissions
    const hasAllPermissions = requiredPermissions.every(permission =>
      user.permissions?.includes(permission)
    );

    if (!hasAllPermissions) {
      throw new ForbiddenError(
        `Missing required permissions: ${requiredPermissions.join(', ')}`
      );
    }

    return true;
  }
}

Usage:

@Controller('posts')
export class PostsController {
  @Post()
  @UseGuards(JwtAuthGuard, PermissionsGuard)
  @RequirePermissions('posts:create')
  create(@Body() createPostDto: CreatePostDto) {
    return this.postsService.create(createPostDto);
  }

  @Delete(':id')
  @UseGuards(JwtAuthGuard, PermissionsGuard)
  @RequirePermissions('posts:delete')
  remove(@Param('id', ParseIntPipe) id: number) {
    return this.postsService.remove(id);
  }
}

API Key Guard

Validate API keys for external integrations:

import { 
  CanActivate, 
  ExecutionContext, 
  Injectable,
  UnauthorizedError,
} from '@hazeljs/core';

@Injectable()
export class ApiKeyGuard implements CanActivate {
  constructor(private apiKeysService: ApiKeysService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const apiKey = request.headers['x-api-key'];

    if (!apiKey) {
      throw new UnauthorizedError('API key is required');
    }

    const isValid = await this.apiKeysService.validate(apiKey);

    if (!isValid) {
      throw new UnauthorizedError('Invalid API key');
    }

    // Optionally attach API key metadata to request
    const keyData = await this.apiKeysService.getKeyData(apiKey);
    request.apiKey = keyData;

    return true;
  }
}

Rate Limiting Guard

Limit the number of requests from a client:

import { 
  CanActivate, 
  ExecutionContext, 
  Injectable,
} from '@hazeljs/core';
import { TooManyRequestsError } from '../exceptions/http.exception';

interface RateLimitOptions {
  windowMs: number;
  maxRequests: number;
}

@Injectable()
export class RateLimitGuard implements CanActivate {
  private requests = new Map<string, number[]>();

  constructor(private options: RateLimitOptions = {
    windowMs: 60000, // 1 minute
    maxRequests: 100,
  }) {}

  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const clientId = this.getClientId(request);
    
    const now = Date.now();
    const windowStart = now - this.options.windowMs;

    // Get existing requests for this client
    let clientRequests = this.requests.get(clientId) || [];
    
    // Filter out old requests outside the window
    clientRequests = clientRequests.filter(time => time > windowStart);
    
    // Check if limit exceeded
    if (clientRequests.length >= this.options.maxRequests) {
      throw new TooManyRequestsError(
        `Rate limit exceeded. Max ${this.options.maxRequests} requests per ${this.options.windowMs / 1000}s`
      );
    }

    // Add current request
    clientRequests.push(now);
    this.requests.set(clientId, clientRequests);

    return true;
  }

  private getClientId(request: any): string {
    // Use IP address or user ID
    return request.user?.id || request.ip || 'anonymous';
  }
}

Usage:

@Controller('api')
export class ApiController {
  @Post('data')
  @UseGuards(new RateLimitGuard({ windowMs: 60000, maxRequests: 10 }))
  postData(@Body() data: any) {
    return this.service.process(data);
  }
}

Combining Multiple Guards

Guards are executed in the order they are listed:

@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard, RateLimitGuard)
export class AdminController {
  // 1. JwtAuthGuard - Verify authentication
  // 2. RolesGuard - Check user role
  // 3. RateLimitGuard - Check rate limit
}

If any guard returns false or throws an exception, the request is denied and subsequent guards are not executed.

Conditional Guards

Create guards that apply conditionally:

import { 
  CanActivate, 
  ExecutionContext, 
  Injectable,
} from '@hazeljs/core';

@Injectable()
export class FeatureFlagGuard implements CanActivate {
  constructor(private featureName: string) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const isEnabled = await this.checkFeatureFlag(this.featureName);
    
    if (!isEnabled) {
      throw new ForbiddenError(`Feature '${this.featureName}' is not enabled`);
    }

    return true;
  }

  private async checkFeatureFlag(feature: string): Promise<boolean> {
    // Check feature flag from database, config, or feature flag service
    return process.env[`FEATURE_${feature.toUpperCase()}`] === 'true';
  }
}

Usage:

@Controller('beta')
export class BetaController {
  @Get('new-feature')
  @UseGuards(new FeatureFlagGuard('new-feature'))
  newFeature() {
    return { message: 'This is a beta feature' };
  }
}

Complete Example

A real-world multi-tenant API using the full guard stack from @hazeljs/auth. No custom guard files or metadata decorators needed.

import { Controller, Get, Post, Patch, Delete, Body, Param, UseGuards } from '@hazeljs/core';
import { JwtAuthGuard, RoleGuard, TenantGuard, CurrentUser } from '@hazeljs/auth';

// Guard order matters:
//   1. JwtAuthGuard  — verify Bearer token, attach req.user
//   2. TenantGuard   — confirm user.tenantId matches :orgId URL param
//   3. RoleGuard     — check req.user.role against the hierarchy
@UseGuards(
  JwtAuthGuard,
  TenantGuard({ source: 'param', key: 'orgId', bypassRoles: ['superadmin'] }),
)
@Controller('/orgs/:orgId/users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  // Any authenticated member of this org can read the list
  @UseGuards(RoleGuard('user'))
  @Get('/')
  findAll(@CurrentUser('sub') userId: string) {
    return this.usersService.findAll();
  }

  // Managers and above can create users
  @UseGuards(RoleGuard('manager'))
  @Post('/')
  create(@Body() dto: CreateUserDto) {
    return this.usersService.create(dto);
  }

  // Only admins can update roles
  @UseGuards(RoleGuard('admin'))
  @Patch('/:id/role')
  updateRole(
    @Param('id') id: string,
    @Body() dto: UpdateRoleDto,
  ) {
    return this.usersService.updateRole(id, dto.role);
  }

  // Only superadmins can delete users
  @UseGuards(RoleGuard('superadmin'))
  @Delete('/:id')
  remove(@Param('id') id: string) {
    return this.usersService.remove(id);
  }
}

The module wires everything together — no guard instances need to be created manually:

import { HazelModule } from '@hazeljs/core';
import { JwtModule } from '@hazeljs/auth';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@HazelModule({
  imports: [
    JwtModule.forRoot({ secret: process.env.JWT_SECRET, expiresIn: '8h' }),
  ],
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule {}

Best Practices

  1. Keep guards focused: Each guard should have a single responsibility
  2. Order matters: Apply guards in the correct order (auth before authz)
  3. Throw descriptive errors: Provide clear error messages
  4. Use dependency injection: Inject services into guards for flexibility
  5. Cache when possible: Cache validation results to improve performance
  6. Test thoroughly: Write tests for all guard scenarios
  7. Document requirements: Clearly document what each guard checks

What's Next?