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 true to allow the request
  • Return false to reject with 403 Forbidden
  • Throw an HttpException for 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