NestJS vs. Ditsmod: DI features for interceptors, guards, pipes and filters
Interceptors, guards, pipes, and filters are sometimes referred to as “enhancers” in the NestJS documentation. Although they are all declared with the @Injectable() decorator and can use Dependency Injection, they are not providers. Because of this, they cannot be exported from the module as normal providers. Ditsmod does not have the filters and pipes mentioned above, since they are essentially interceptors, so there is no technical need to create a separate architectural element. Also, in Ditsmod, interceptors and guards are providers, so they can be exported from modules like other providers.
In this post, NestJS v9.2 and Ditsmod v2.27 are used for comparison. I am the author of Ditsmod.
I decided to write this article when I was browsing NestJS issues, in particular A Guard with dependencies can’t be injected into another module. Its author wonders why NestJS cannot resolve dependency for AuthGuard, because this dependency is imported into the module where the guard is declared.
As the creator of NestJS wrote, it turns out that the guard is not a provider:
Enhancers can’t be injected to other providers. Hence, they are not providers. Likewise, enhancers can be tied & injected to the method evaluation context, but you can’t bind providers this way. There’s no reason to change this, especially if you start thinking about enhancers scoped per host (module in which they exist). Adding them to the providers array explicitly creates an unnecessary redundancy and confusion (since enhancers don’t act equally to providers).
So, adding interceptors, guards, pipes and filters to the list of providers will confuse NestJS users. I think, quite a strange logic, which is argued by strange solutions, for example, the impossibility of injecting providers at the route level. Why can’t such injections be made? What is the technical reason for this?
As a result of these restrictions, NestJS must export all dependencies for guards from the module, even if those dependencies are not directly used outside of the module where the guards are declared. This restrictions essentially breaks module encapsulation.
In Ditsmod providers can be passed to injectors at four levels:
- application
- module
- route
- request
import { featureModule } from '@ditsmod/core';
@featureModule({
providersPerApp: [],
providersPerMod: [],
providersPerRou: [],
providersPerReq: [],
// ...
})
export class SomeModule {}
And the guards in Ditsmod are normal providers that are passed to the injector at the request level:
import { featureModule } from '@ditsmod/core';
import { AuthGuard } from 'auth.guard';
@featureModule({
providersPerReq: [AuthGuard],
})
export class SomeModule {}
This code demonstrates the centralized addition of a guard to a module, and by the way, NestJS v9 still doesn’t support this feature. In NestJS, adding a guard is supported only at the controller or method level, because the guard is not considered a provider.