Nestjs Dependency Injection and DDD / Clean Architecture
Asked Answered
S

5

41

I'm experimenting with Nestjs by trying to implement a clean-architecture structure and I'd like to validate my solution because I'm not sure I understand the best way to do it. Please note that the example is almost pseudo-code and a lot of types are missing or generic because they're not the focus of the discussion.

Starting from my domain logic, I might want to implement it in a class like the following:

@Injectable()
export class ProfileDomainEntity {
  async addAge(profileId: string, age: number): Promise<void> {
    const profile = await this.profilesRepository.getOne(profileId)
    profile.age = age
    await this.profilesRepository.updateOne(profileId, profile)
  }
}

Here I need to get access to the profileRepository, but following the principles of the clean architecture, I don't want to be bothered with the implementation just now so I write an interface for it:

interface IProfilesRepository {
  getOne (profileId: string): object
  updateOne (profileId: string, profile: object): bool
}

Then I inject the dependency in the ProfileDomainEntity constructor and I make sure it's gonna follow the expected interface:

export class ProfileDomainEntity {
  constructor(
    private readonly profilesRepository: IProfilesRepository
  ){}

  async addAge(profileId: string, age: number): Promise<void> {
    const profile = await this.profilesRepository.getOne(profileId)
    profile.age = age

    await this.profilesRepository.updateOne(profileId, profile)
  }
}

And then I create a simple in memory implementation that let me run the code:

class ProfilesRepository implements IProfileRepository {
  private profiles = {}

  getOne(profileId: string) {
    return Promise.resolve(this.profiles[profileId])
  }

  updateOne(profileId: string, profile: object) {
    this.profiles[profileId] = profile
    return Promise.resolve(true)
  }
}

Now it's time to wiring everything together by using a module:

@Module({
  providers: [
    ProfileDomainEntity,
    ProfilesRepository
  ]
})
export class ProfilesModule {}

The problem here is that obviously ProfileRepository implements IProfilesRepository but it's not IProfilesRepository and therefore, as far as I understand, the token is different and Nest is not able to resolve the dependency.

The only solution that I've found to this is to user a custom provider to manually set the token:

@Module({
  providers: [
    ProfileDomainEntity,
    {
      provide: 'IProfilesRepository',
      useClass: ProfilesRepository
    }
  ]
})
export class ProfilesModule {}

And modify the ProfileDomainEntity by specifying the token to use with @Inject:

export class ProfileDomainEntity {
  constructor(
    @Inject('IProfilesRepository') private readonly profilesRepository: IProfilesRepository
  ){}
}

Is this a reasonable approach to use to deal wit all my dependencies or am I completely off-track? Is there any better solution? I'm new fairly new to all of these things (NestJs, clean architecture/DDD and Typescript as well) so I might be totally wrong here.

Thanks

Suspender answered 24/10, 2018 at 12:30 Comment(1)
any advantage of using abstract classes(+no default functionality) over interface (+ string provider) ? or opposite.Kalat
I
29

It is not possible to resolve dependency by the interface in NestJS due to the language limitations/features (see structural vs nominal typing).

And, if you are using an interface to define a (type of) dependency, then you have to use string tokens. But, you also can use class itself, or its name as a string literal, so you don't need to mention it during injection in, say, dependant's constructor.

Example:

// *** app.module.ts ***
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AppServiceMock } from './app.service.mock';

process.env.NODE_ENV = 'test'; // or 'development'

const appServiceProvider = {
  provide: AppService, // or string token 'AppService'
  useClass: process.env.NODE_ENV === 'test' ? AppServiceMock : AppService,
};

@Module({
  imports: [],
  controllers: [AppController],
  providers: [appServiceProvider],
})
export class AppModule {}

// *** app.controller.ts ***
import { Get, Controller } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  root(): string {
    return this.appService.root();
  }
}

You also can use an abstract class instead of an interface or give both interface and implementation class a similar name (and use aliases in-place).

Yes, comparing to C#/Java this might look like a dirty hack. Just keep in mind that interfaces are design-time only. In my example, AppServiceMock and AppService are not even inheriting from interface nor abstract/base class (in real world, they should, of course) and everything will work as long as they implement method root(): string.

Quote from the NestJS docs on this topic:

NOTICE

Instead of a custom token, we have used the ConfigService class, and therefore we have overridden the default implementation.

Intervocalic answered 29/10, 2018 at 7:23 Comment(0)
C
51

Export a symbol or a string along with your interface with the same name

export interface IService {
  get(): Promise<string>  
}

export const IService = Symbol("IService");

Now you can basically use IService as both the interface and the dependency token

import { IService } from '../interfaces/service';

@Injectable()
export class ServiceImplementation implements IService { // Used as an interface
  get(): Promise<string> {
    return Promise.resolve(`Hello World`);
  }
}
import { IService } from './interfaces/service';
import { ServiceImplementation} from './impl/service';
...

@Module({
  imports: [],
  controllers: [AppController],
  providers: [{
    provide: IService, // Used as a symbol
    useClass: ServiceImplementation
  }],
})
export class AppModule {}
import { IService } from '../interfaces/service';

@Controller()
export class AppController {
  // Used both as interface and symbol
  constructor(@Inject(IService) private readonly service: IService) {}

  @Get()
  index(): Promise<string> {
    return this.service.get(); // returns Hello World
  }
}
Consentaneous answered 23/11, 2021 at 23:15 Comment(3)
I loved this approach since it keeps the DI clean and the interfaces files are responsible for managing their symbols.Without
For all who were also surprised about a const and an interface with the same name: #49798656Saying
It seems to me that defining IService as value in providers: [{ provide: IService, // Used as a symbol useClass: ServiceImplementation }], will make compiler upset. I think it should be string otherwise Error: 'IService' only refers to a type, but is being used as a value hereOutsider
I
29

It is not possible to resolve dependency by the interface in NestJS due to the language limitations/features (see structural vs nominal typing).

And, if you are using an interface to define a (type of) dependency, then you have to use string tokens. But, you also can use class itself, or its name as a string literal, so you don't need to mention it during injection in, say, dependant's constructor.

Example:

// *** app.module.ts ***
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AppServiceMock } from './app.service.mock';

process.env.NODE_ENV = 'test'; // or 'development'

const appServiceProvider = {
  provide: AppService, // or string token 'AppService'
  useClass: process.env.NODE_ENV === 'test' ? AppServiceMock : AppService,
};

@Module({
  imports: [],
  controllers: [AppController],
  providers: [appServiceProvider],
})
export class AppModule {}

// *** app.controller.ts ***
import { Get, Controller } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  root(): string {
    return this.appService.root();
  }
}

You also can use an abstract class instead of an interface or give both interface and implementation class a similar name (and use aliases in-place).

Yes, comparing to C#/Java this might look like a dirty hack. Just keep in mind that interfaces are design-time only. In my example, AppServiceMock and AppService are not even inheriting from interface nor abstract/base class (in real world, they should, of course) and everything will work as long as they implement method root(): string.

Quote from the NestJS docs on this topic:

NOTICE

Instead of a custom token, we have used the ConfigService class, and therefore we have overridden the default implementation.

Intervocalic answered 29/10, 2018 at 7:23 Comment(0)
S
10

You can indeed use interfaces, well abstract classes. One typescript feature is inferring interface from classes (which are kept in JS world), so something like this will work

IFoo.ts

export abstract class IFoo {
    public abstract bar: string;
}

Foo.ts

export class Foo 
    extends IFoo
    implement IFoo
{
    public bar: string
    constructor(init: Partial<IFoo>) {
        Object.assign(this, init);
    }
}
const appServiceProvider = {
  provide: IFoo,
  useClass: Foo,
};

Stedt answered 12/2, 2019 at 21:40 Comment(4)
Is it OK for Foo to only implement IFoo without extending it too, provided the abstract class has not shared logic and merely is a inject-via-interface hack in TypeScript?Stenson
Yes it is OK for you to implement only.Stedt
any advantage of using abstract classes(+no default functionality) over interface (+ string provider) ? or opposite.Kalat
in this instance, you will need to use an abstract class to keep it (therefore the reference for the DI) in the JS world, as interface simply aren't transpiledStedt
R
4

I use a different approach that helps preventing naming collision accross multiple modules.

I use string tokens along with a custom decorator to hide implementation details:

// injectors.ts
export const InjectProfilesRepository = Inject('PROFILES/PROFILE_REPOSITORY');

// profiles.module.ts
@Module({
  providers: [
    ProfileDomainEntity,
    {
      provide: 'PROFILES/PROFILE_REPOSITORY',
      useClass: ProfilesRepository
    }
  ]
})
export class ProfilesModule {}

// profile-domain.entity.ts
export class ProfileDomainEntity {
  constructor(
    @InjectProfilesRepository private readonly profilesRepository: IProfilesRepository
  ){}
}

It's more verbose but multiple services can be safely imported from different modules with the same name.

Rozamond answered 16/8, 2021 at 12:51 Comment(0)
A
1

As a side note:

If you follow the DDD/Clean architecture, you should not access the repository from the domain entity.

A usecase class or a domain service will use the repository to get the domain entity, then you act on it and when you finish the same usecase/domain service will store the domain entity.

The domain entity sits at the center of the architecture diagram and it should not depend on anything else.

Acidic answered 8/5, 2022 at 11:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.