Guards & Interceptors
Guards
Guards run before the route handler and decide whether the request is allowed through. They implement a canActivate method.
Writing a Guard
import { Context } from "@rune/core";import { Injectable } from "@rune/decorators";
@Injectable("transient")export class AuthGuard { canActivate(ctx: Context): boolean { const token = ctx.request.headers.get("authorization"); return token !== null && token.startsWith("Bearer "); }}Async Guard
@Injectable("transient")export class AdminGuard { async canActivate(ctx: Context): Promise<boolean> { const auth = ctx.request.headers.get("authorization"); if (!auth) return false; const user = await verifyToken(auth.slice(7)); return user.role === "admin"; }}Using Guards
import { Controller, Get, UseGuard } from "@rune/decorators";
// Class-level: applies to all routes@UseGuard(AuthGuard)@Controller("/admin")export class AdminController { @Get("/dashboard") dashboard() { return { secret: "data" }; }
// Method-level: additional guard @Get("/super-secret") @UseGuard(SuperAdminGuard) superSecret() { return { level: "top secret" }; }}Module-Level Guards
Guards defined in the module’s providers array whose class name ends with Guard or which implement canActivate are automatically applied to all controllers in that module:
@Module({ controllers: [AdminController], providers: [AuthGuard, AdminGuard],})export class AdminModule {}Guard Execution Order
- Module-level guards (discovered from providers)
- Class-level guards (
@UseGuardon the controller class) - Method-level guards (
@UseGuardon the route method)
If any guard returns false, the request is rejected with a 403 Forbidden response.
Interceptors
Interceptors wrap the route handler execution, allowing you to transform the result or perform side effects.
Writing an Interceptor
import { Context } from "@rune/core";import { Injectable } from "@rune/decorators";
@Injectable("transient")export class LoggingInterceptor { async intercept(ctx: Context, next: () => Promise<Response>): Promise<Response> { console.log("Before handler"); const result = await next(); console.log("After handler"); return result; }}Response Transformation
@Injectable("transient")export class WrapInterceptor { async intercept(_ctx: Context, next: () => Promise<Response>): Promise<Response> { const response = await next(); const body = await response.json(); const wrapped = { data: body, timestamp: new Date().toISOString() }; return new Response(JSON.stringify(wrapped), { status: response.status, headers: response.headers, }); }}Using Interceptors
import { Controller, Get, UseInterceptor } from "@rune/decorators";
// Class-level: applies to all routes@UseInterceptor(LoggingInterceptor)@Controller("/api")export class ApiController { @Get("/public") publicData() { return { public: true }; }
// Method-level interceptor @Get("/wrapped") @UseInterceptor(WrapInterceptor) wrappedData() { return { secret: "wrapped" }; }}Interceptor Pipeline
Multiple interceptors are composed as a chain. The outermost interceptor runs first:
@UseInterceptor(LoggingInterceptor, WrapInterceptor)// Execution: LoggingInterceptor wraps WrapInterceptor wraps handlerGuard + Interceptor Interaction
Guard rejection happens before any interceptor runs:
- Module guards check → reject 403 if failed
- Class guards check → reject 403 if failed
- Method guards check → reject 403 if failed
- Interceptor chain runs
- Route handler executes