Skip to content

Build1 publisher2 min readPublished

NestJS global guards built with new run every request without their injected services

NestJS guards passed to useGlobalGuards as hand-built instances run with undefined services on every request, a NestJS 12 tutorial shows. Registering through APP_GUARD fixes it. The tutorial's own code leaves open whether the broken guard lets requests through or throws.

The Engineer · Build desk

Illustration accompanying NestJS global guards built with new run every request without their injected services

What happened

  • A NestJS tutorial describes an admin controller protected by a RolesGuard that injects UserService and is also registered globally with app.useGlobalGuards(new RolesGuard(...)) in main.ts.
  • useGlobalGuards() takes an instance, and one built with new in main.ts skips Nest's dependency injection because it exists before any providers do.
  • The tutorial says userService was undefined inside the guard on every request while the app kept running without a crash.
  • The walkthrough targets NestJS 12.0.x and @nestjs/core 12.0.4, and says the guard APIs involved have been stable since before version 9.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • decision Global guards that inject services have to be re-registered as APP_GUARD providers, and the tutorial's fix leaves the guard classes themselves untouched.
  • constraint Smoke tests and health checks that only hit routes without roles metadata will pass against a broken build, so only a request to a role-protected route surfaces the defect.
  • contradiction A team cannot grade severity from this source alone: the throwing case is an outage on admin routes, and only the no-op case could become an authorization bypass.

The listing shows where the fault sits. Its `main.ts` line is `app.useGlobalGuards(new RolesGuard(new Reflector(), /* userService? */ undefined))`, so the service argument is a literal `undefined` [6]. Nest calls `canActivate()` on that object for every request [5]. It cannot give the object the `UserService` its constructor declared [5].

The tutorial says the line was written that way because "global" sounded like the right word for "runs on every route" [15]. For scope, it is [5]. According to the tutorial, the guard class and the decorator are both correct, and the registration is the only fault [16]. In its model, a guard is an ordinary dependency-injection provider, the same kind of class as a service, that also implements `canActivate(context: ExecutionContext)` for Nest to call just before the route handler [9]. "A guard only gets real dependency injection if Nest constructs it," the tutorial says [11].

It is harder to say what the broken guard does to a request. The tutorial says it "let every request through the branch that assumes a user has no elevated role" [2]. Read literally, that sentence has every user treated as unprivileged. The code comment in the same listing covers more ground: with `userService` undefined, "this branch always throws or always no-ops" [8]. The tutorial's lifecycle rule decides the first case. A guard that throws or resolves `false` stops the handler, and Nest passes the request to the exception-filter layer [10]. A throwing guard therefore refuses the request with an error, and the process keeps running [14]. A request gets past the check only if the missing-dependency path resolves `true`, and the excerpt does not show code that does that.

One line lets requests through on purpose. `if (!required) return true;` passes any handler without `roles` metadata before the code reaches `userService` [7]. The undefined service is touched only on routes that declare roles [17].

I think the review scope is small. A hand-built instance loses only what its constructor asked the container for. So a `useGlobalGuards(new ...)` call is a problem when the guard injects a service and is harmless when it injects nothing [18].

What to watch

  • A reproduction of the scenario showing whether the controller-level @UseGuards(RolesGuard) copy still blocked non-admin requests while the global copy was broken.
  • A published variant of the guard whose missing-dependency path resolves true, the case that would make this an authorization bypass instead of an error on admin routes.
  • A NestJS change that warns at startup when useGlobalGuards receives an instance with undefined constructor dependencies.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories