Today we will discuss what the dependency injection is. The literal meaning of this term is to inject dependencies. The dependency is just another object that your class needs to function. So if you have a model class "UserModel" that fetches data from a database object, then we can say that your model class depends on the database object.

So now that we know what a dependency is, let's talk about what it means to inject dependencies. Injecting dependencies just means that the dependency is pushed into the class from the outside. All that means is that you shouldn't instantiate dependencies using the new operator from inside of the class, but instead, you should take it as a constructor parameter or via a setter. That's really all there is to dependency injection. You don't need a fancy container or a class or an object to do it. The DI containers may make your life easier but you can not say that you absolutely need them.

But why should we inject dependencies? Let's imagine for a minute that you're programming a CurrencyConverter class. The class has a facade that consists of only one method "convert". But what you also need inside this class is some way to retrieve currency rates from different sources. So how would you retrieve the currency rates, would you implement this logic inside the "CurrencyConverter" class or would you implement a separate class for this purpose? Implementing this logic in the CurrencyConverter class would lead to breaking the SRP principle from SOLID, and the CurrencyConverter would be polluted with unrelated logic. The most flexible way to do it would be to retrieve currency rates from a separate class. And that's exactly where the dependency injection does help us. It decouples your classes' construction from the construction of its dependencies.
class CurrencyRateService {

    getRate(source: ICurrency, dest: ICurrency): number {
        ....
    }

}

class CurrencyConverterService {

    constructor(private currencyRateService: CurrencyRateService) {}

    convert(amount: number, source: ICurrency, dest: ICurrency) {
        return amount * this.currencyRateService->getRate(source: ICurrency, dest: ICurrency); 
    }

}
The reason why this is so important is the dependency inversion principle. Basically dependency inversion is the principle that code should depend upon abstractions. By depending upon abstractions we're decoupling our implementations from each other. In this case, we can substitute different dependencies as long as they all satisfy the required interface. By using dependency injection we decouple our code from the lower-level implementations making our code cleaner, easier to modify and easier to reuse.

Now that we've adopted the dependency injection we have another problem. Each of our classes requires all of these dependencies so now to construct each and every class we not only need to figure out what dependencies they need, we need to figure out how to instantiate the dependencies. Luckily for us, there's a solution for this, and this solution is a Dependency Injection Container. The DI container is nothing more than a map of dependencies that your class needs with the logic to create those dependencies if they haven't been created yet. The map can be stored in some dependencies config files or even in the DIC class itself. If you what to look at custom DIC implementation in node, you can take a look at this repo node-service-container.

So every time you ask for dependency the map will figure out which dependency to use and then the container will check to see if it has created one of those dependencies already. If it has, it'll just use that one otherwise it'll create the dependency, store it and then return it. So instead of constructing all of your classes yourself, you ask the container for a new instance, it will then resolve the dependencies, construct your object and return it to you.

The best part of it is that the container can resolve complex dependencies transparently and if you want to swap out a generic dependency you only need to update the container.

So use the dependency injection and write a cleaner and more decoupled code.