Understanding Dependency Injection in NestJS
--
This blog is not an introduction to NestJS, but its more of how NestJS uses dependency injection.
To understand dependency injection, we first need to understand things around Inversion of Control Principle.
It states that Classes should not create instances of its dependencies on its own.
Let’s dive this with 3 different examples / pattern
The Bad example
export class MessageService {
messagesRepo: MessagesRepository
constructor() {
this.messagesRepo = new MessagesRepository()
}
}
In this example here, the MessageService
class creates its own instance of MessagesRepository
. It is not following the Inversion of Control principle. So this is a bad example.
The Better example
export class MessageService {
messagesRepo: MessagesRepository
constructor(repo: MessagesRepository) {
this.messagesRepo = repo
}
}
In this example, the constructor receives the messagesRepo
instance and it does not need to worry about the instantiation of MessagesRepository
object. But the slight downside is that the constructor always specifically needs MessagesRepository
object always to be passed in.
The Best example
interface Repository {
findOne(id: String);
findAll();
create();
}
export class MessageService {
messagesRepo: Repository
constructor(repo: Repository) {
this.messagesRepo = repo
}
}
In this example also, the constructor takes in an argument. But this time it can be any object that implements the Repository
interface. It can either be MessagesRepository
or it can be any other, but it should satisfy the interface.
How would that be helpful?
Consider this scenario. In the development / testing environment, when MessageService
instance is being instantiated, it would have a MessageRepository
object as part of the constructor. If you are aware of NestJS concepts then you very well know that repositories usually interact with databases. And invoking the databases usually means there is some IO and takes some time to process it.
In testing environment (may be automated tests), we want to run test scripts which needs to run fast. We might not want to do actual database calls. So the MessageService
in test environment will be instantiated with a FakeMessageRepository
object which has implementation of the interface Repository
without the database calls. This would make the tests extremely fast.
In short, the MessageService
can be instantiated with any object that satisfies the interface.
Some other benefits of Inversion of Control principle:
- Decoupling and Modularity: This promotes modularity and improves the system’s maintainability, scalability, and reusability.
- Testability: By allowing dependencies to be injected or provided externally, it becomes easier to substitute real implementations with mock objects for testing purposes.
- Flexibility and Extensibility: New components can be added or existing components can be replaced without modifying the existing code, simply by configuring the IoC container appropriately.
- Configuration Management: IoC containers often manage configuration details, making it easier to change settings without modifying the application code. This supports the principle of “separation of concerns” by keeping configuration separate from the application logic.
- Centralized Control: IoC containers provide a centralised mechanism for managing object creation and lifecycle. This centralisation can simplify the overall design and promote consistent behaviour across the application.
But…..
Its not so beautiful always. Again, if you are familiar with NestJS you would know that in order to create a controller instance, you would need to create a service instance and a repository instance
const messageRepo = new MessagesRepository()
const messageService = new MessageService(messageRepo)
const messagesController = new MessagesController(messageService)
As an application grows in the size, a controller might take in multiple services. Each service might take in different instances of repositories.
const messageRepo = new MessagesRepository()
const userRepo = new UserRepo()
const messageService = new MessageService(messageRepo)
const userService = new UserService(userRepo, messageRepo)
const messsagesController = new MessagesController(userService, messagesService)
This complexity would increase if multiple service would need multiple repositories or a controller would need multiple services. The amount of code to instantiate a single controller would be too much.
Seems like the Best is not the best. If somehow we could make use of the Better method (i.e. the second approach) to do things in IoC way, it would be great.
Dependency Injection to the rescue
Dependency Injection is all about making use of IoC and not having to create a ton of different instances every time you need to create a controller.
NestJS maintains a Dependency Injection Container (also called Injector). This an object with couple of different properties in it. For simplicity, it maintains 2 different sets of information
i. List of classes and their dependencies
ii. List of instances that have been created
This is how it works:
- The NestJS app starts up
- It looks up for all the classes other than the controller. It checks the
MessageService
, it looks at its constructor and understands that it has a dependency toMessagesRepository
. - It adds
MessageService
to the list of classes and dependency section - It looks at the constructor of
MessagesRepository
, and finds that it has no dependency. It addsMessagesRepository
to the list of classes and dependency section - It creates an instance of
MessagesRepository
and adds it to the list of instances section - Next it creates the instance of
MessagesService
since it now has the instance ofMessagesRepository
and adds it to the list of instances section - Next it instantiates the controller. The controller can now be instantiated since the dependency of
MessageService
is already available.
DI Container Flow
- At start-up, register all classes with the container
- Container will figure out what each dependency each class has
- Whenever its needed, the container is requested to create an instance of a class for us
- Container creates all required dependencies and gives us the instance
- Container will hold on to the created dependency instances and re-use them if needed
The @Injectable
marks the registration of the class into the DI container. We do not register controller as part of DI container, because controller is a consumer. The instantiation of controller is taken care by Nest.
In the module file, when you define a providers array, you are actually defining the things that can be used as dependencies for other classes.
// message.service.ts
import { Repository } from 'typeorm';
@Injectable()
export class MessageService {
messagesRepo: MessagesRepository
constructor(@InjectRepository(Message) private repo: Repository<Message>) {}
}
// messages.entity.ts
@Entity()
export class Message {
}
// messages.module.ts
@Module({
controllers: [MessagesController],
// things that can be used as dependencies for other classes
providers: [MessageService],
imports: [TypeOrmModule.forFeature([Message])],
})
export class MessagesModule {}
The @InjectRepository
decorator may be a little bit weird and out of sync with the @Injectable
way of things. But what its saying is that instance repo
is of type TypeORM Repository
that deals with instances of Message
, hence the generics is used Repository<Message>
to denote the same. The @InjectRepository
decorator is an aid to dependency injection which tells the DI container that it needs the Message
repository.
The DI figures out the dependencies and gives the instances during run time. But unfortunately, the DI is not good with generics and we use the @InjectRepository
because we are using the generic type.
Hope this gave an idea of how DI works.
Thanks for reading. Any comments feedback are always welcome ❤️