Guards
Control access to routes based on conditions like authentication and roles.
Guards determine whether a request should be handled by the route handler. They are the recommended way to implement authentication and authorization in MiiaJS.
CanActivate interface
A guard is a class that implements the CanActivate interface:
import type { CanActivate, RequestContext } from '@miiajs/core'
class AuthGuard implements CanActivate {
canActivate(ctx: RequestContext): boolean | Promise<boolean> {
const token = ctx.req.headers.get('authorization')
return !!token
}
}
- Return
trueto allow the request - Return
falseto reject with403 Forbidden - Throw an
HttpExceptionfor a custom error response
Applying guards
Per route
import { UseGuard } from '@miiajs/core'
@Controller('/items')
class ItemController {
@Delete('/:id')
@UseGuard(AuthGuard)
remove(ctx: RequestContext) {
return { deleted: true }
}
}
Per controller
@Controller('/admin')
@UseGuard(AuthGuard)
class AdminController {
@Get('/dashboard')
dashboard() {
return { stats: {} }
}
}
Global
const app = new Miia()
.useGuard(AuthGuard)
.register(AppModule)
Guards with DI
Guards can use dependency injection:
@Injectable()
class AuthGuard implements CanActivate {
private tokenService = inject(TokenService)
async canActivate(ctx: RequestContext): Promise<boolean> {
const header = ctx.req.headers.get('authorization')
if (!header?.startsWith('Bearer ')) {
throw new UnauthorizedException()
}
const user = await this.tokenService.verify(header.slice(7))
if (!user) throw new UnauthorizedException()
ctx.user = user
return true
}
}
Parameterized guards
Create guard factories for reusable logic:
function Roles(...roles: string[]) {
class RolesGuard implements CanActivate {
canActivate(ctx: RequestContext) {
return roles.includes(ctx.user?.role)
}
}
return RolesGuard
}
Combine multiple guards:
@Delete('/:id')
@UseGuard(AuthGuard, Roles('admin'))
remove(ctx: RequestContext) {
return { deleted: true }
}
Guards execute in order. If AuthGuard fails, Roles is never called.
Skipping guards
Use @SkipGuard() to exclude specific guards at compile time. The guard middleware is not added to the route pipeline at all - zero runtime overhead:
import { SkipGuard } from '@miiajs/core'
@Controller('/posts')
@UseGuard(AuthGuard())
class PostController {
@Get('/')
@SkipGuard(AuthGuard) // AuthGuard is not applied to this route
list() { return [] }
@Get('/:id') // AuthGuard applies normally
findOne(ctx: RequestContext) { ... }
}
@SkipGuard works with direct guard classes, factory guards (like AuthGuard()), and global guards registered via app.useGuard(). Multiple guards can be skipped: @SkipGuard(GuardA, GuardB).
@Injectable()
class GlobalAuth implements CanActivate {
canActivate(_ctx: RequestContext) { return false }
}
const app = new Miia().useGuard(GlobalAuth).register(AppModule)
@Controller('/webhooks')
class WebhookController {
@Post('/stripe')
@SkipGuard(GlobalAuth) // bypass the app-level guard for this endpoint
stripe(ctx: RequestContext) {
return { received: true }
}
}
Global guards do not run on unmatched (404) routes - there is no resource to authorize, and this avoids leaking information about which auth scheme protects a path that doesn't exist. Global middleware (
app.use()) still runs on 404s.Guard execution order
Global guards (app.useGuard(), filtered by @SkipGuard)
-> Controller guards (@UseGuard on class)
-> Route guards (@UseGuard on method)
-> Route handler