Typescript Dependency Injection, the easy way
Dependency injection is an extremely useful tool in applications of any size. The goal of this article isn’t to explain what dependency injection is, or why you should use it (you probably should use it 🤣). If you need to learn about the what’s and why’s, these articles are an excellent place to start.
— The Wikipedia page
— Dependency Injection in TypeScript
I’ve tried many different dependency injection techniques in my own applications. These techniques can range from borderline intimidating (Angular 2 dependency injection is hardcore nuts), to easy and straightforward. This article aims at the latter.
Project Structure
The root of our project is an index.ts file in our src folder (or the root of whatever your Typescript folder is). This is the primary entry point for our code.
We want the structure of our project to visually represent the different sections that are in our project. This is accomplished by using a folder hierarchy.
A section can be whatever we want, as long as it makes sense. For example, I might have a section of services that I call from another module. Or I could have a section of interfaces that I use throughout my application. This is a really general idea, but understanding how to break up your application into sections can make complex applications easier to understand.
Each of these sections are exported as a module. This module will export all references and instances of the items in this section that your application will need. This makes referencing items very clean and creates the foundation for dependency injection.
Now here is the primary question of this article. What happens when an item in one section depends on an item in another section? Lets say Class B depends on Class A. The question, asked in a slightly different way, is: How does Class A get injected into Class B?
Well, remember, we are instantiating and exporting all section items as a module. So, when we instantiate Class B, import the instance of Class A from it’s respective module and inject it into Class B.
Hands on Example
If you read the above giant block of text, congrats. This idea is really hard to understand via text alone. Let’s look at a concrete example.
This example is a simple ExpressJS server that GETs users by an id and POSTs new users. Our project structure looks like this.
Here are my reasons for creating an endpoints and services section.
Endpoints: This section will hold all of the business logic for each endpoints. It will not contain any code about connecting to our database, or even care what kind of database we are using. We should be able to completely change our database system without editing this section
Services: This sections contains all of the connection information for our database, and will be responsible for adding new users and getting existing users. This section will also hold a service for sending welcome emails to new users.
Let’s setup our basic index.ts file.
The above code creates a very basic ExpressJS server on port 3000. It has a route to GET a customer by their id and a route to POST a new customer.
Services
Let’s create a file in our services folder called database.ts and write a stub for our database service.
This is a very blank code file for now. Feel free to fill in those functions to setup a connection with whatever database you choose to use. For now I’m going to leave these functions blank.
I’m going to do the same thing with an EmailSender class. Create a file called emailSender.ts in the services folder.
Again, these functions are empty. This will allow us to call the appropriate methods in our endpoints but implement the functionality later. Feel free to fill these functions in with any emailer service.
Now we need to wrap up these services into a module. Create a file called index.ts in the services folder. This is the crucial file for our module.
This does two things.
It exports the class references from this file. That means when I reference the class Database later on, I can import it directly from the services folder without needing to reference the database.ts file.
It also exports out instantiated versions of theses services. These consts are what we will be injecting into our endpoints in the next step.
If you take anything away from this article, make it the above two sections.
Endpoints
Create a file called getCustomer.ts in the endpoints folder.
We export a function called makeGetCustomer, when in turn returns an async function called getCustomer. This function getCustomer is what our ExpressJS endpoint will use.
The business logic here is very straightforward. Attempt to get the customer from the database, if successful return the customer, else log out the error.
Note that we are importing the Database class reference directly from the services folder.
Now create a file called postCustomer.ts in the endpoints folder.
Now that we have the business logic written for our two endpoints, create an index.ts file in the endpoints folder.
This looks very similar to our services index file, except we don’t have any class references to export.
We are importing the db and emailSender consts that we instantiate and export in our services module. This is the how module based dependency injection works. Both the getCustomer and postCustomer endpoint are using the same Database instance.
This is how the module based dependency injection works
Now let’s wrap a nice and tidy bow on it. Update the index.ts file in the root to look like this.
Conclusion
This pattern of dependency injection is extremely powerful because of the high level of code splitting that it makes available to us. We are able to completely rethink our database setup without needing to modify any business logic, same for our EmailSender.
When I write large applications I am constantly thinking about scalability and readability. I use the above pattern because (I believe) it accomplishes a high degree of scalability, but does so in a very readable way.
Let me know what you think of this approach!