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 proceedfalse- 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
- Keep guards focused: Each guard should have a single responsibility
- Order matters: Apply guards in the correct order (auth before authz)
- Throw descriptive errors: Provide clear error messages
- Use dependency injection: Inject services into guards for flexibility
- Cache when possible: Cache validation results to improve performance
- Test thoroughly: Write tests for all guard scenarios
- Document requirements: Clearly document what each guard checks
What's Next?
- Learn about Middleware for request preprocessing
- Understand Interceptors to transform responses
- Explore Exception Filters for error handling
- Add Pipes for data validation and transformation