Step up your developper game SOLID-ifying your codebase
Tags: typescript, nestjs, solid, architecture
Published: 2021-05-16 12:10:38

---
title: Step up your developper game SOLID-ifying your codebase
published: true
description: A quick practical guide to applying SOLID principles to an existing NestJS codebase. 
tags: Typescript, NestJs, SOLID, Architecture
cover_image: https://www.centrale-canine.fr/sites/default/files/styles/race_top_image_1346x540/public/2018-09/Abenchedshow_0.jpg
---

So recently a friend of mine asked me for help on a [NestJs](https://nestjs.com/) project. He was facing a strong coupling between the business services and [TypeORM-related code](https://docs.nestjs.com/techniques/database).

```typescript
// subscription.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from 'entities/user.entity';
import { Company } from 'entities/company.entity';
import { Job } from 'entities/job.entity';

@Injectable()
export class SubscriptionService {
  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
    @InjectRepository(Company)
    private readonly companyRepository: Repository<Company>,
    @InjectRepository(Job)
    private readonly jobRepository: Repository<Job>,
  ) {}

  // business logic happening here
}
```

Theses services grew up to have more and more Typeorm repositories injected with the `@InjectRepository` annotation, and were eventually really hard to test with all the mocks involved.

```typescript
// subscription.service.test.ts

import { Test } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { mock } from 'jest-mock-extended';
import { Repository } from 'typeorm';
// [...]

describe('The subscription service', () => {
  let subscriptionService: SubscriptionService;
  const userRepositoryMock = mock<Repository<User>>();
  const companyRepositoryMock = mock<Repository<Company>>();
  const jobRepositoryMock = mock<Repository<Job>>();

  beforeEach(async () => {
    const moduleRef = await Test.createTestingModule({
        controllers: [],
        providers: [
          {
            provide: getRepositoryToken(User),
            useValue: userRepositoryMock,
          },
          {
            provide: getRepositoryToken(Company),
            useValue: companyRepositoryMock,
          },
          {
            provide: getRepositoryToken(Job),
            useValue: jobRepositoryMock,
          },
          // other TypeORM related injections
        ],
      }).compile();

    subscriptionService = moduleRef.get<SubscriptionService>(SubscriptionService);
  });

  // Tests on the subscriptionService and the dependencies mocks
};
```

Instantiating the `SubscriptionService` is quite cumbersome, as we need to spawn a whole NestJs module to do so.

A more naive approach would be to instantiate the `SubscriptionService` with something like `new SubscriptionService(/* mocked dependencies */)`, but note that even if some wonderful packages such as [jest-mock-extended](jest-mock-extended) help mocking dependencies, Typescript will struggle accepting TypeORM repositories mocks as replacement for the mocked services.

Using such repository mocks involves too much abstraction Typescript can't handle and yields an `Expression produces a union type that is too complex to represent` error.

# SOLID principles to the rescue

After a quick discussion about what part of the business this `SubscriptionService` was addressing, we came to the conclusion that at least the following SOLID principle was broken here:
- **Liskov substitution principle**: the `SubscriptionService` knows too much about the entity repositories it relies on. It should not be aware of TypeORM being used to handle database access. Should I change my data access logic, my business services must not suffer from any code change

# Separation of concerns

## Decoupling data access from business logic

Having a clear idea of what the problem was, I started thinking about what should be improved with the design. The `SubscriptionService` must be decoupled from my ORM: let's draw the separating line with a few interfaces:

```typescript
// user.repository.ts

import { User } from 'entities/user.entity';

export interface UserRepository {
  findOne(/* ... */): Promise<User>;

  // Other user fetching methods
}
```

{% details Company and job repositories %}

```typescript
// company.repository.ts

import { Company } from 'entities/company.entity';

export interface CompanyRepository {
  findOne(/* ... */): Promise<Company>;

  // Other user fetching methods
}
```

```typescript
// job.repository.ts

import { Job } from 'entities/job.entity';

export interface JobRepository {
  findOne(/* ... */): Promise<Job>;

  // Other user fetching methods
}
```

{% enddetails %}

We can now update our `SubscriptionService` to make use of these repositories:

```typescript
// subscription.service.ts

import { Injectable } from '@nestjs/common';
import { UserRepository } from 'repositories/user.repository';
import { CompanyRepository } from 'repositories/company.repository';
import { JobRepository } from 'repositories/job.repository';

@Injectable()
export class SubscriptionService {
  constructor(
    private readonly userRepository: UserRepository,
    private readonly companyRepository: CompanyRepository,
    private readonly jobRepository: JobRepository,
  ) {}
  
  // business logic happening here
}
```

Testing the service is now way simpler, we don't need any NestJs utility to instantiate the system under test— `SubscriptionService`—:

```typescript
// subscription.service.test.ts

import { mock } from 'jest-mock-extended';
import { UserRepository } from 'repositories/user.repository';
import { CompanyRepository } from 'repositories/company.repository';
import { JobRepository } from 'repositories/job.repository';

describe('The subscription service', () => {
  const userRepositoryMock = mock<UserRepository>();
  const companyRepositoryMock = mock<CompanyRepository>();
  const jobRepositoryMock = mock<JobRepository>();
  const subscriptionService = new SubscriptionService(
    userRepositoryMock,
    companyRepositoryMock,
    jobRepositoryMock,
  );

  // Tests on the subscriptionService and the dependencies mocks
};
```

## Implementing the newly created interfaces

In order to make our application work again, we need to use the TypeORM repositories to implement the interfaces we just defined. We can create a concrete implementation for `UserRepository` as follows:

```typescript
// user.typeorm-repository.ts

import { UserRepository } from 'repositories/user.respository';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from 'entities/user.entity';

export class UserTypeormRepository implements UserRepository {
  constructor(
    @InjectRepository(User)
    private readonly userORMRepository: Repository<User>,
  ) {}

  findOne(/* ... */): Promise<User> {
    return this.userORMRepository.findOne(/* ... */);
  }

  // Other method implementations
}
```

Same goes for the `CompanyRepository` and `JobRepository` implementations, I will not go into unnecessary details.

## Using NestJs service container to put everything together

Now we have on one side our `SubscriptionService` using repository interfaces, and concrete interface implementations on the other side.

A typical way to plug the two together is to use a service container, that will take care of injecting concrete implementations where interfaces are required.

Let's use NestJs service container to do so!

Since Typescript interfaces do not exist in Javascript, we need to help NestJs match an interface with its implementation using what NestJs calls [providers](https://docs.nestjs.com/providers).

We will use simple strings defined along our repositories as unique keys to help dependency injection:

```typescript
// user.repository.ts

import { User } from 'entities/user.entity';

export interface UserRepository {
  // ...
}

export const userRepositoryToken = Symbol('UserRepository');
```

> Note that we use a `Symbol` here to ensure the token is unique in the service container.

We can now register the concrete implementation into NestJs service container:

```typescript
// user.module.ts

import { Module } from '@nestjs/common';
import { userRepositoryToken } from 'repositories/user.repository';
import { UserTypeormRepository } from 'infrastructure/typeorm/user.typeorm-repository';

@Module({
  controllers: [/* .. */],
  providers: [
    {
      provide: userRepositoryToken,
      useClass: UserTypeormRepository,
    },
    // other providers
  ],
})
export class UserModule {}
```

Same goes for `CompanyModule` and `JobModule`.

## Injecting the repositories

Concrete implementations of the repository interfaces being available in the service container, we can now fix `SubscriptionService` dependency injection:

```typescript
// subscription.service.ts

import { Inject, Injectable } from '@nestjs/common';
import { UserRepository, userRepositoryToken } from 'repositories/user.repository';
import { CompanyRepository, companyRepositoryToken } from 'repositories/company.repository';
import { JobRepository, jobRepositoryToken } from 'repositories/job.repository';

@Injectable()
export class SubscriptionService {
  constructor(
    @Inject(userRepositoryToken)
    private readonly userRepository: UserRepository,
    @Inject(companyRepositoryToken)
    private readonly companyRepository: CompanyRepository,
    @Inject(jobRepositoryToken)
    private readonly jobRepository: JobRepository,
  ) {}
  
  // business logic happening here
}
```

# Wrapping it up

Using a clear interface to separate technical concerns from business logic is a first step towards making the codebase easy to maintain and evolve.

Unit testing is simplified, it is quicker and safer to refactor the `SubscriptionService`.

We can now move on to other SOLID principles, if we were to realize that our `SubscriptionService` does not respect the **single responsibility principle**, it would not cost much to split it into smaller services.