nestjs-guard-map
v0.1.0
Published
Runtime guard, interceptor and pipe map for NestJS — audit every route's security metadata at a glance
Maintainers
Readme
nestjs-guard-map
Runtime route security map for NestJS. At application startup, it scans every route and prints a complete picture of which guards, interceptors and pipes are applied — then optionally fails your CI if any route is left unprotected.
╔══════════════════════════════════════════════════╗
║ nestjs-guard-map — Route Security ║
╚══════════════════════════════════════════════════╝
Global guards: JwtGuard
UsersController
GET /users [JwtGuard]
GET /users/:id [JwtGuard, RolesGuard] Roles: ["admin","user"]
POST /users [JwtGuard, RolesGuard] Roles: ["admin"]
DELETE /users/:id [JwtGuard, RolesGuard] Roles: ["admin"]
HealthController
GET /health [public]Why
NestJS has no built-in way to audit which routes are protected. In large applications, guards spread across controller decorators, method decorators, and global APP_GUARD providers — making it easy to accidentally expose a route. Static analysis is infeasible because NestJS resolves decorator metadata at runtime.
nestjs-guard-map uses DiscoveryModule + MetadataScanner + Reflector to build an accurate, complete security map after your application fully bootstraps.
Installation
npm install nestjs-guard-mapPeer dependencies (already in your project):
npm install @nestjs/common @nestjs/core reflect-metadataSetup
// app.module.ts
import { Module } from '@nestjs/common';
import { GuardMapModule } from 'nestjs-guard-map';
@Module({
imports: [
GuardMapModule.forRoot(),
// ... your modules
],
})
export class AppModule {}That's it. On the next npm run start:dev, the security map prints to the console.
Configuration
GuardMapModule.forRoot({
// Where to emit the report. Default: ['console']
output: ['console', 'json'],
// Path for JSON file. Default: './guard-map-report.json'
outputPath: './reports/security-map.json',
// Metadata key used by your @Public() decorator, if any
publicMetadataKey: 'isPublic',
// Extra metadata fields to include per route (e.g. @Roles())
customMetadata: [
{ key: 'roles', label: 'Roles' },
{ key: 'throttle', label: 'Rate Limit' },
],
// Audit mode — detect unprotected routes
audit: {
// Fail if any of these HTTP methods has no guard
requireAuthOn: ['POST', 'PUT', 'PATCH', 'DELETE'],
// true → throw at startup (fail fast in CI)
// false → warn to console (default)
throwOnViolation: false,
// Paths exempt from the audit (supports * wildcard)
allowlist: ['/health', '/metrics/*'],
},
})CI security gate
Set throwOnViolation: true to make the application refuse to start if any unprotected route is detected:
GuardMapModule.forRoot({
audit: {
requireAuthOn: ['POST', 'PUT', 'PATCH', 'DELETE'],
throwOnViolation: process.env.NODE_ENV === 'production',
allowlist: ['/health', '/auth/login', '/auth/register'],
},
})Output on violation:
Error: [nestjs-guard-map] Audit failed — 2 unprotected route(s):
POST /users (UsersController.create)
└─ POST route has no guard and is not marked as public
DELETE /users/:id (UsersController.remove)
└─ DELETE route has no guard and is not marked as public@Public() decorator support
If you use a @Public() decorator to mark routes as intentionally unauthenticated, pass its metadata key to publicMetadataKey. Those routes will show as [public] instead of [NO GUARD ⚠] and will be excluded from audit violations.
// your public.decorator.ts
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
// app.module.ts
GuardMapModule.forRoot({
publicMetadataKey: IS_PUBLIC_KEY,
})Custom metadata fields
Display any metadata set by your own decorators — roles, permissions, rate limits:
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
// guard-map config
customMetadata: [{ key: ROLES_KEY, label: 'Roles' }]Output:
POST /admin/users [JwtGuard, RolesGuard] Roles: ["admin"]APP_GUARD detection
Guards registered globally via APP_GUARD are detected automatically and displayed at the top of the report:
Global guards: JwtGuard, ThrottlerGuardRoutes covered by a global guard are considered protected even if they have no method-level guard.
JSON report
With output: ['json'], a structured RouteReport is written to disk on every startup. Useful for diffing security posture between deployments:
{
"routes": [
{
"method": "POST",
"path": "/users",
"controller": "UsersController",
"handler": "create",
"guards": ["JwtGuard", "RolesGuard"],
"interceptors": ["LoggingInterceptor"],
"pipes": [],
"customMetadata": { "Roles": ["admin"] },
"isPublic": false
}
],
"globalGuards": ["ThrottlerGuard"],
"generatedAt": "2026-06-07T16:00:00.000Z"
}How it works
onApplicationBootstrap runs after all NestJS modules, providers and guards are fully resolved — which is after NestFactory.create() and after app.listen() triggers app.init(). At that point the full dependency graph is available.
RouteDiscoveryService uses DiscoveryService.getControllers() to iterate all controller wrappers, reads controller-level metadata (__guards__, __interceptors__, __pipes__) via Reflector, then uses MetadataScanner.getAllMethodNames() to iterate route handlers and read method-level metadata. Global guards are found by scanning DiscoveryService.getProviders() for the APP_GUARD token.
License
MIT
