Dependency Inversion Principle in NestJS with TypeORM

Yvan Florian
4 min readMay 9, 2023

We’ll look at how we can implement the dependency inversion principle (DIP) in NestJS using TypeORM with Postgres. This is what the principle states:

  1. Higher-level modules should not import anything from lower-level modules. Both should depend on abstractions.
  2. Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.

So what does this mean in the context of NestJS and TypeORM? (There are guides, like this one, to help think about DIP in NestJS, but I couldn’t find any dealing with the persistence layer in general and TypeORM in particular)

The example that we find in the official docs follow the simple CRUD way, whereby, if you’re using REST, you have a controller that injects a service. The service itself depends on the persistence layer; details of which are handled by TypeORM. This is all fine, as far as small applications go. But this tightly-coupling is what DIP is against (for the purposes of maintenance and scalability). We have higher-level modules (controllers/services) depending on lower-level modules (TypeORM Entities/Repositories) instead of abstractions.

So how do we apply DIP in this case?

Let’s take an example of a CustomerModule that has a controller to create and retrieve customers in the database. Normally, we’d have a CustomerController that calls a CustomerService . Within the service we’d inject a TypeORM Repository to interact with the database as below

import { Injectable } from "@nestjs/common"
import { InjectRepository } from "@nestjs/typeorm"
import { Repository } from "typeorm"
import { Customer } from "./customer.entity"
import { CreateCustomerDTO } from "./create-customer.dto.ts"

@Injectable()
export class CustomerService{
constructor(
@InjectRepository(Customer)
private readonly customerRepository: Repository<Customer>
) {}

async findById(id: string): Promise<Customer> {
return await this.customerRepository.findOne({ where: { id: id } })
}

async findAll(): Promise<Customer[]> {
return await this.customerRepository.find()
}

async create(props: CreateCustomerDTO): Promise<Customer> {
return await this.customerRepository.save(props)
}
}

Instead of doing this, as explained above, we’d create an abstraction in the middle, that both the service and the lower level TypeORM implementations are going to depend on, and thereby inverting, as it were, the dependencies.

How the dependency is “inverted”
How the dependency is “inverted”

The simple implementation will have our directory looking like this:

End-Result Directory

We’ve abstracted the persistence inside the customer.interface.ts and here is what we have:

import { Customer, CustomerProps } from "./customer"

export const CustomerInterfaceToken = Symbol("CustomerInterface")
export interface CustomerInterface {
findById: (id: string) => Promise<Customer | null>
findAll: () => Promise<Customer[]>
create: (pro: CustomerProps) => Promise<Customer>
}

Then, our service (which our controller is calling) can now depend on the interface in this way.

import { Inject, Injectable } from "@nestjs/common"
import { CustomerInterface, CustomerInterfaceToken } from "./customer.interface"
import { Customer, CustomerProps } from "./customer"

@Injectable()
export class CustomerService {
constructor(
@Inject(CustomerInterfaceToken)
private readonly customerRepo: CustomerInterface
) {}

async findById(id: string): Promise<Customer | null> {
return this.customerRepo.findById(id)
}

async findAll(): Promise<Customer[]> {
return this.customerRepo.findAll()
}

async create(customer: CustomerProps): Promise<Customer> {
return this.customerRepo.create(customer)
}
}

As a note here, The Customer & CustomerProps in this case are also abstraction of the customer model. They don’t depend on anything.

Below is how we’re going to write a repository which implements our interface.


export class CustomerRepository implements CustomerInterface {
constructor(
@InjectRepository(CustomerEntity)
private readonly customerRepo: Repository<CustomerEntity>
) {}

async findById(id: string): Promise<Customer | null> {
const customer = await this.customerRepo.findOne({ where: { id: id } })
return customer
}

async findAll(): Promise<Customer[]> {
return await this.customerRepo.find()
}

async create(props: CustomerProps): Promise<Customer> {
const customer = new Customer(props)
return await this.customerRepo.save(customer)
}
}

Now, the final bit, is how we connect the implementation with the abstraction: By registering our repository implementation, in our list of providers, together of course with the CustomerService.

import { Module } from "@nestjs/common"
import { CustomerController } from "./customer.controller"
import { TypeOrmModule } from "@nestjs/typeorm"
import { CustomerInterfaceToken } from "./customer.interface"
import { CustomerRepository } from "./customer.repository"
import { CustomerEntity } from "./customer.entity"
import { CustomerService } from "./customer.service"

@Module({
imports: [TypeOrmModule.forFeature([CustomerEntity])],
controllers: [CustomerController],
providers: [
CustomerService,
{
provide: CustomerInterfaceToken,
useClass: CustomerRepository,
},
],
})
export class CustomerModule {}

There we go. We now have DIP implemented to abstract away the persistence layer. This way, it will be way easier (because of the loosely coupled architecture ) should I want to change my persistence infrastructure in the future: and this is one of the main advantage of the principle

For reference, here’s a GitHub repo for your browsing. Hope it helps.

--

--