Dependency injection (DI) is a software design pattern that allows a developer to remove hard-coded dependencies and make their code more flexible and easy to test. In this article, we will discuss how to implement typescript dependency injection in a Node.js application.
First, let’s define what dependency is. A dependency is a piece of code that another piece of code relies on to function correctly. For example, a service that sends emails may depend on a library that handles the actual sending of the email. The service is the dependent code, and the email-sending library is the dependency.
In traditional software development, dependencies are often hard-coded into the dependent code. This makes the code difficult to test and less flexible. With dependency injection, the dependent code is given its dependencies through its constructor or a setter method, rather than hard-coding them. This allows for easy testing and flexibility in the application.
Dependency Injection can help us:
- Write flexible classes
- Easily test our code
- Reduce the amount of boilerplate code
- Improve the readability of our code
Let us first understand what problem dependency injection solves. Let’s consider an API call that returns an array. The request flow has the following steps:
- The request comes to the Controller and it handles all the routing.
- The controller calls the Service, and it handles all the business logic.
- The service calls the Repository, and it handles all the database calls.
Here, the controller has a dependency on the service, and the service has a dependency on the Repository. This is a typical dependency in Node.js applications.
If we want to use the Service class, we will have to create an instance of Repository inside the Service class.
Creating one class instance inside another causes tight coupling and is not a good practice.
Say we want to test our Service class. Do we want to have an interaction between the test code and the actual database?
Absolutely Not — we would mock the database calls and thereby test our Service class. Otherwise:
- We will have to create a test database
- Our test suite has a dependency on the database. The test suite will break if something breaks in the database
- The test suite will be very slow
So we need a way to inject the instance of Repository into the Service class. Here comes the importance of typescript dependency injection.
We can implement this in NodeJS using InversifyJS. InversifyJS is a thin inversion of control (IoC) container for JS and TypeScript applications. InversifyJS uses the inversion of control principle to give you more control over the way your components are created, configured, and managed. There are several other ways to implement inversion of control as follows:
- Using a factory pattern
- Using a service locator pattern
- We can use a dependency injection of any given below type
- A constructor injection
- A setter injection
- An interface injection
Let’s first understand what is Inversion of Control (IOC).
In traditional programming, objects that are statically assigned to one another determine the flow of the business logic. With inversion of control, the flow depends on the object graph that is instantiated by the assembler and is made possible by object interactions being defined through abstractions. The binding process is achieved through typescript dependency injection, and some may argue that inversion of control can also be provided by the use of a service locator.
Inversion of control serves the following purposes:
This implementation decouples the execution of certain tasks. Every module can focus on its design purpose. Modules just rely on their contracts and make no assumptions about what other systems do. Replacing modules has no side effects on other modules.
Let’s see how it’s done.
Installation:
First, we need to install Inversify in our project:
npm install inversify reflect-metadata
Next, we need to enable the use of decorators in our TypeScript configuration by adding “experimentalDecorators”: true and “emitDecoratorMetadata”: true in the “compilerOptions” section of our tsconfig.json file.
{
“compilerOptions”: {
“target”: “es5”,
“lib”: [“es6”],
“types”: [“reflect-metadata”],
“module”: “commonjs”,
“moduleResolution”: “node”,
“experimentalDecorators”: true,
“emitDecoratorMetadata”: true
}
}
InversifyJS requires a modern Javascript engine with support for:
- Reflect metadata
- Map
- Promise
- Proxy
The Setup:
InversifyJS recommends putting dependencies in an inversify.config.ts file. Only this place has some coupling. Let’s go ahead and add our typescript Dependency Injection container there:
// inversify.config.ts
import { Container } from “inversify”;
import { TYPES } from “./types”;
import { Rider, Bike} from “./entities”;
let container = new Container();
container.bind<Rider>(TYPES.Rider).to(Rider);
container.bind<Bike>(TYPES.Bike).to(Bike);
We can use the method get<T> from the container class to resolve a dependency. Here we use get<Rider>.
let rider = container.get<Rider>(TYPES.Rider);
InversifyJS at runtime uses the type as identifiers. We use symbols as identifiers. We can also use classes and or string literals.
// types.ts
export const TYPES = {
Rider: Symbol(“Rider”),
Bike: Symbol(“Bike”)
};
Declare dependencies using the @injectable & @inject decorators. All the classes should mandatory be annotated with the @injectable decorator.
A class having a dependency on an interface will need to use the @inject decorator to define an identifier for the interface that will be available at runtime.
// entities.ts
import { inject, injectable } from “inversify”;
import { TYPES } from “./types”;
@injectable()
export class Rider {
private _bike: Bike;
constructor(@inject(TYPES.Bike) bike: Bike) {
this._bike = bike;
}
public ride() {
return `Using a ${this._bike.throttle()}`;
}
public throw(){
return `Throwing a ${this._bike.throttle()}`;
}
}
export interface Bike{
throttle(): string;
}
@injectable()
export class UnrideableBike implements Bike{
public throttle() {
return “unrideable bike”;
}
}
In this example, we use InversifyJS to define two types of dependencies: Rider and Bike. The Rider class has a dependency on the Bike interface. We use the inject decorator to specify the dependencies in the constructor of the Rider class and use the bind method on the container to associate the Rider and Bike types with their corresponding implementations. We then use the get method on the container to retrieve an instance of the Rider class and call its ride method, which in turn uses the throttle method on the Bike instance to produce the output.
In this way, we can easily implement dependency injection in a Node.js TypeScript application using the Inversify library. This makes our code more flexible, easier to test, and easier to maintain.