Achieve dependency inversion with NodeJS, Typescript, and tsyringe

Matheus Hofstede
3 min readSep 29, 2022

--

Dependency inversion is one of the 5 SOLID principles and, in my opinion, one of the most important as it allows decoupling modules through abstractions instead of using concrete implementations.

Remember that you don’t need to use a dependency injection tool to achieve the dependency inversion principle, although a DI library like tsyringe makes it easier by delegating the injection to an external module/container.

At various times in my career, I came across projects in js/ts that used a DI library, and although a repository/service was being injected, the injection was provided by the class name (and not by an abstraction).

container.register(InMemmoryCatsRepository, InMemmoryCatsRepository)...
export class GetCatsUseCase implements UseCase {
@inject(InMemmoryCatsRepository) private catsRepository: CatsRepository
...
}

Injecting InMemmoryCatsRepository (a class) breaks the idea of dependency inversion as the usecase layer gets to know the implementation, thus depending directly on it.

Instead of using a class like before, let’s use a token, which can be a class, a string, or a symbol.

type InjectionToken<T = any> =
| constructor<T>
| DelayedConstructor<T>
| string
| symbol;

To show that, I will use a project that uses dependency injection manually and implement it correctly with tsyringe.

Application Architecture

Folder schema

Starting from the point where the dependencies are injected manually (via instance), let’s focus on the part where the dependency inversion makes the most sense: in the usecase.

And dependencies are resolved in index.ts

See this commit

At this point, we have the benefit that the usecase constructor receives an interface, and that's great. But we can do even better by injecting the dependencies using a dependency injection tool.

Let's follow with using tsyringe.

I'm not going to show how to install and configure tsyringe, as it is well documented in the official docs.

Let's start by creating a container to register the dependencies in src/di/container.ts

Now all dependencies are registered in one single place, and we can control and register each instance depending on the environment (e.g., injecting a FakeRepository in a test environment)

Let's annotate the usecase with @injectable, and inject the repository with @inject. The repository also needs to be annotated with @injectable.

See this commit.

Although the code above works, we just created a dependency between GetCatsUseCase and InMemmoryCatsRepository. In other words, an inner layer (application) depends on an implementation detail, the infra layer (database).

https://www.dandoescode.com/blog/clean-architecture-an-introduction/

Let's solve this issue.

First, we must attach a token with the repository (abstraction). I choose to use a Symbol as they are immutable and unique.

And the repository can be registered as a Singleton, so the instance is shared between all the application lifetime:

And now, our usecase does not know (and it doesn't need to) the concrete repository implementation.

See this commit.

The implementation of that Repository can be like this:

And changing the Repository Data Source is as easy as:

  1. Implementing a new repository:

2. And registering it:

Check this commit.

The application works in the same way, but now we are fully respecting the dependency inversion principle, and in addition, respecting the open-closed principle as a Repository is open for extension but closed for modification.

This tiny example can be extended at various levels and be applied to more significant projects, with the benefit of having much more maintainable applications.

--

--