NestJS Guards Are Opt-In: Keeping New Routes From Shipping as Public Endpoints
0 reputation · 01 Nov 2023, 14:18 UTC
0 reputation · 01 Nov 2023, 14:18 UTC
NestJS guards implement CanActivate and deny a request when they return false or throw, which makes them the standard tool for route protection. The documented limitation is scope: a guard only runs on the controller or handler where it is bound, so any route that is never bound remains reachable without checks.
The goal is a default-deny HTTP surface: every route authenticated unless it is explicitly marked public. NestJS supports registering a guard globally through the APP_GUARD token, and the common pattern pairs that global guard with a metadata marker that exempts selected routes. What is not settled is how far that guarantee extends in practice.
Specific open points:
APP_GUARD execute for every HTTP route in the application, including lazily loaded modules and versioned routes?31775 reputation · 01 Nov 2023, 19:32 UTC
Yes — a guard registered through the APP_GUARD token gives you the default-deny surface you want, and the three open points all have workable answers. The short version: global guards bind per route handler at registration time (so lazy modules and versioned routes are covered), guards run broadest-to-narrowest with an AND semantic (so a controller guard cannot undo a global denial), and metadata reflection can fail closed if you treat anything other than strict true as protected.
An APP_GUARD registration is bound by the framework to every HTTP route handler as routes are registered, not per module import. That means routes in lazily loaded modules and routes produced by URI or header versioning are guarded the same way as eagerly loaded ones. The caveats that actually bite people are elsewhere:
Guard execution order is broadest to narrowest: global first, then controller-scoped, then method-scoped. Every guard in the chain must allow the request — the semantics are AND, not override. A controller-level guard can add restrictions but can never rescue a request the global guard already denied. That is exactly the property you want for default-deny: no downstream annotation can accidentally re-open a route.
The standard pattern is a global guard that consults an IS_PUBLIC key via Reflector.getAllAndOverride, checking the handler first and falling back to the controller class:
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic !== true) {
// enforce authentication
}
Your worry about "no marker" versus "marker set to false" is mostly unfounded: SetMetadata stores exactly the value given, and an unset key reflects as undefined. Comparing with !== true means both undefined (forgotten decorator) and false fail closed. The one real trap is the inverse case: getAllAndOverride prefers the handler-level value when the key exists there, so if a class is marked @Public() and one method must be authenticated, that method must explicitly set the key to false — relying on absence won't override the class-level true.
undefined, false, and true; only true should activate.One caveat: guard ordering and the Reflector API are version-sensitive across NestJS major releases, so confirm against the version pinned in your project before shipping.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.