This chapter provides an overview of the Dependency Injection (DI) pattern, which is frequently used today by the biggest frameworks. It is a way to keep code clean and easier to use. By using this pattern you end up with fewer coupled components and more reusable ones, which helps accelerate the development process time.
Here we examine the method that used the injection before the pattern existed, and how the injection changed in time to use Nest.js injection with a modern approach using TypeScript and decorators. You will also see snippets that show the advantage of this type of pattern, and modules provided by the framework.
Nest.js is based on Angular in terms of architecture, and is used to create testable, scalable, loosely-coupled and easily maintainable applications. As is the case with Angular, Nest.js has its own dependency injection system, which is part of the core of the framework, meaning that Nest.js is less dependent on a third-party library.
Since Typescript 1.5 introduces the notion of the decorator, you can do meta-programing using the added metadata provided by using a
decorator on different objects or properties, such as class, function, function parameters or class property. The meta-programing is the ability to write some code or program using the metadata describing an object. This type of program allows you to modify the functioning of a program using its own metadata. In our case this metadata is of interest to us, because it helps inject some object into another object, whose name is Dependency Injection.
By using the decorator, you can add metadata on any object or property linked to those decorators. This will define, for example, the type of object that takes the decorator, but it can also define all of the parameters
needed by a function that are described in its metadata. To get or define metadata on any object, you can also use the reflect-metadata library in order to manipulate them.
The real interest in using Dependency Injection is that the objects will be less coupled between the dependent and its dependencies. With the framework that provides the injector system, you can manage your objects without thinking about the instanciation of them, because that is managed by the injector, which is there to resolve the dependencies of every dependent object.
This means that it is easier to write tests and mock dependencies, which are much cleaner and more readable.
Let’s imagine an AuthenticationService that needs a UserService to be injected.
Here is the UserService:
exportclassUserService() {privateusers:Array<User>=[{id:1,:'userService1@email.com',password:'pass']};publicfindOne({where}:any):Promise<User>{returnthis.users.filter(u=>{returnu.===where.&&u.password===where.password;});}}
And the AuthenticationService, which instantiates the UserService that is needed:
exportclassAuthenticationService{publicuserService:UserService;constructor(){this.userService=newUserService();}asyncvalidateAUser(payload:{string;password:string}):Promise<boolean>{constuser=awaitthis.userService.findOne({where:payload});return!!user;}}
constauthenticationService=newAuthenticationService();
As you can see, you have to manage all of the related dependencies in the class itself to be used inside the AuthenticationService.
The disadvantage of this is mostly the inflexibility of the AuthenticationService. If you want to test this service,
you have to think about its own hidden dependencies, and of course, you can’t share any services between different classes.
Let’s see now how you can pass dependencies through the constructor using the previous UserService.
// Rewritted AuthenticationServiceexportclassAuthenticationService{/*Declare at the same time the publicproperties belongs to the class*/constructor(publicuserService:UserService){}}
// Now you can instanciate the AutheticationService like thatconstuserService=newUserService();constauthenticationService=newAuthenticationService(userService);
You can easily share the userService instance through all of the objects, and it is no longer the AuthenticationService, which has to create a UserService instance.
This makes life easier because the injector system will allow you to do all of this without needing to instantiate the dependencies. Let’s see this using the previous class in the next section.
Today, to use Dependency Injection, you just have to use the decorator system provided by Typescript and implemented by the framework that you want to use. In our case, as you will see in the Tools chapter, Nest.js provides some decorators that will do almost nothing except add some metadata on the object or property where they will be used.
This metadata will help make the framework aware that those objects can be manipulated, injecting the needed dependencies.
Here is an example of the usage of the @Injectable() decorator:
@Injectable()exportclassUserService{/*...*/}@Injectable()exportclassAuthenticationService{constructor(privateuserService:UserService){}}
This decorator will be transpiled and will add some metadata to it. This means that you have accessed design:paramtypes after using a decorator on the class, which allows the injector to know the type of the arguments that are dependent on the AuthenticationService.
Generally, if you would like to create your own class decorator, this one will take as parameter the target that represents the type of your class. In the previous example, the type of the AuthenticationService is the AuthenticationService itself. The purpose of this custom class decorator will be to register the target in a Map of services.
exportComponent=()=>{return(target:Type<object>)=>{CustomInjector.set(target);};}
Of course, you have seen how to register a services into a Map of service, so let’s look at how this could be a custom injector. The purpose of this injector will be to register all of the services into a Map, and also to resolve all the dependencies of an object.
exportconstCustomInjector=newclass{protectedservices:Map<string,Type<any>>=newMap<string,Type<any>>();resolve<T>(target:Type<any>):T{consttokens=Reflect.getMetadata('design:paramtypes',target)||[];constinjections=tokens.map(token=>CustomInjector.resolve<any>(token));returnnewtarget(/*...*/injections);}set(target:Type<any>){this.services.set(target.name,target);}};
So, if you would like to instanciate our AuthenticationService, which depends on the super UserService class, you should call the injector in order to resolve the dependencies and return this instance of the wanted object.
In the following example, we will resolve through the injector the UserService that will be passed into the constructor of the AuthenticationService in order to be able to instanciate it.
constauthenticationService=CustomInjector.resolve<AuthenticationService>(AuthenticationService);constisValid=authenticationService.validateUser(/* payload */);
From the @nestjs/common you have access to the decorators provided by the framework and one of them is the @Module() decorator. This decorator is the main decorator to build all of your modules and work with the Nest.js Dependency Injection system between them.
Your application will have at least one module, which is the main one. The application can use only one module (the main one) in the case of a small app. Nonetheless, as your app grows, you will have to create several modules to arrange your app for the main module.
From the main module, Nest will know all of the related modules that you have imported, and then create the application tree to manage all of the Dependency Injections and the scope of the modules.
To do this, the @Module() decorator respects the ModuleMetadata interface,
which defines the properties allowed to configure a module.
exportinterfaceModuleMetadata{imports?:any[];providers?:any[];controllers?:any[];exports?:any[];modules?:any[];// this one is deprecated.}
To define a module, you have to register all of the services stored in providers that will be instantiated by the Nest.js injector, as well as the controllers that can inject the providers, which are services, registered into the module or those exported by another module through the exports property. In such a case, these have to be registered in imports.
It is not possible to access an injectable from another module if it has not been exported by the module itself, and if the exporting module hasn’t been imported into the concerned module, which has to use the external services.
How does Nest.js create the Dependency injection tree?
In the previous section, we talked about the main module, generally called AppModule, which is used to create the app from NestFactory.create. From here, Nest.js will have to register the module itself, and it will also go through each module imported to the main module.
Nest.js will then create a container for the entire app, which will contain all of the module, globalModule, and dynamicModuleMetadata of the entire application.
After it has created the container, it will initialize the app and, during the initialization, it will instantiate an InstanceLoader and a DependenciesScanner -> scanner.ts, via which Nest.js will have the possibility to scan every module and metadata related to it. It does this to resolve all of the dependencies and generate the instance of all modules and services with their own injections.
If you want to know the details of the engine, we recommend that you go deep into the two classes: InstanceLoader and DependenciesScanner.
To have a better understanding of how this works, take a look at an example.
Imagine that you have three modules:
The app will be created from the ApplicationModule:
@Module({imports:[UserModule,AuthenticationModule]})exportclassApplicationModule{/*...*/}
This imports the AuthenticationModule:
@Module({imports:[UserModule],providers:[AuthenticationService]})exportclassAuthenticationModule{/*...*/}@Injectable()exportclassAuthenticationService{constructor(privateuserService:UserService){}}
And the UserModule:
@Module({providers:[UserService],exports:[UserService]})exportclassUserModule{/*...*/}@Injectable()exportclassUserService{/*...*/}
In this case, the AuthenticationModule must import the UserModule, which exports the UserService.
We have now built our application’s architecture module and have to create the app, which will be allowed to resolve all of the dependencies.
constapp=awaitNestFactory.create(ApplicationModule);
Essentially, when you create the app, Nest.js will:
modules metadata.What about the global module?
Nest.js also provides a @Global() decorator, allowing Nest to store them in a global Set of modules, which will be added to the related Set of the module concerned.
This type of module will be registered with the __globalModule__ metadata key and added to the globalModule set of the container. They will then be added to the related Set of the module concerned. With a global module, you are allowed to inject components from the module into another module without importing it into the targeted module. This avoids having to import a module, which is possibly used by all of the modules, into all of the modules.
Here is an example:
@Module({imports:[DatabaseModule,UserModule]})exportclassApplicationModule{/*...*/}
@Global()@Module({providers:[databaseProvider],exports:[databaseProvider]})exportclassDatabaseModule{/*...*/}
@Module({providers:[UserService],exports:[UserService]})exportclassUserModule{/*...*/}@Injectable()exportclassUserService{// SequelizeInstance is provided by the DatabaseModule store as a global moduleconstructor(@Inject('SequelizeInstance')privatereadonlysequelizeInstance){}}
With all the previous information, you should now be familiar with the mechanism of the Nest.js dependency injection and have a better understanding of how they work together.
Even if Nest.js is widely based on Angular, there is a major difference between them. In Angular, each service is a singleton, which is the same as Nest.js, but there is a possibility to ask Angular to provide a new instance of the service.
To do that in Angular, you can use the providers property of the @Injectable() decorator to have a new instance of a provider registered in the module and available only for this component. That can be useful to have to avoid overwriting some properties through different components.
So to recap, we have seen in this chapter how it was unflexible and hard to test an object without using the Dependecy Injection. Also, we have learned more about the evolution of the method to implement the dependencies into the dependent, first by implementing the dependencies into the dependent, then changing the method by passing them manually into the constructor to arrive with the injector system. This then resolves the dependencies, injecting them in the constructor automatically by resolving a tree, which is how Nest.js uses this pattern.
In the next chapter we will see how Nest.js uses TypeORM, an Object Relational Mapping (ORM) that works with several different relational databases.