Exploring the Synergy of Nestjs Dependency Injection with Domain-Driven Design and Clean Architecture

I am currently exploring Nestjs and experimenting with implementing a clean-architecture structure. I would appreciate validation of my approach as I am unsure if it is the most effective way to do so. Please note that the example provided is somewhat pseudo-code, and many types are left out or generalized as they are not the main focus of this discussion.

Starting from my domain logic, one way to implement it could be 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)
  }
}

Accessing the profileRepository, however, goes against the principles of clean architecture. To address this, an interface is created for it:

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

The next step is to inject this dependency into the constructor of the ProfileDomainEntity and ensure it follows 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)
  }
}

Subsequently, a simple in-memory implementation is created to test 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, everything needs to be wired together using a module:

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

An issue arises where ProfileRepository implements IProfilesRepository but there is no direct match between them. This leads to Nest not being able to resolve the dependency properly.

The workaround found is to use a custom provider to manually set the token:

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

This requires modifying the ProfileDomainEntity by specifying the token to use with @Inject:

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

Is this a reasonable way to handle dependencies, or am I missing the mark entirely? Are there better solutions available? Being relatively new to NestJs, clean architecture/DDD, and Typescript, I may have misunderstood some concepts here.

Thank you

Answer №1

Include a symbol or a string, sharing the same name as your interface

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

export const IService = Symbol("IService");

Now you can utilize IService for both the interface and dependency token

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

@Injectable()
export class ServiceImplementation implements IService { // Acting 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 symbolic representation
    useClass: ServiceImplementation
  }],
})
export class AppModule {}
import { IService } from '../interfaces/service';

@Controller()
export class AppController {
  // Serving the dual role of interface and symbol
  constructor(@Inject(IService) private readonly service: IService) {}

  @Get()
  index(): Promise<string> {
    return this.service.get(); // returns Hello World
  }
}

Answer №2

Resolving dependencies by the interface in NestJS is not feasible due to certain limitations in the language features (refer to structural vs nominal typing).

If you are using an interface to define a dependency type, you will need to use string tokens. However, it is also possible to use the class itself or its name as a literal string so that explicit injection is not required in the dependent's constructor.

For 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 can also consider using an abstract class instead of an interface, or have both the interface and implementation class share similar names (with aliases used in place).

While this approach may seem unconventional compared to C# or Java practices, keep in mind that interfaces are for design-time purposes only. In the provided example, AppServiceMock and AppService do not necessarily inherit from an interface or abstract/base class, yet everything functions smoothly as long as they implement the method root(): string.

The NestJS documentation on custom providers emphasizes:

NOTICE

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

Answer №3

Using interfaces or abstract classes in TypeScript can be quite advantageous. One interesting feature is the ability to infer interfaces from classes, as demonstrated below:

ExampleInterface.ts

export abstract class ExampleInterface {
    public abstract property: string;
}

ExampleClass.ts

export class ExampleClass extends ExampleInterface implements ExampleInterface {
    public property: string;
    
    constructor(init: Partial<ExampleInterface>) {
        Object.assign(this, init);
    }
}
const serviceConfig = {
  provide: ExampleInterface,
  useClass: ExampleClass,
};

Answer №4

My strategy for avoiding naming conflicts across various modules involves a unique approach.

By utilizing string tokens in conjunction with a custom decorator, I am able to obscure 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
  ){}
}

Although it may be more verbose, this method allows for the safe importation of multiple services with identical names from separate modules.

Answer №5

Just a quick side note to consider:

In the context of DDD/Clean architecture principles, it is recommended not to directly access the repository from within the domain entity.

Instead, a usecase class or a domain service should be responsible for utilizing the repository to retrieve the domain entity, performing actions on it, and then storing the updated entity once completed.

The domain entity should remain at the core of the architectural design without relying on external dependencies.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Learn how to pass JSON data into an anchor tag with Angular router link and the query parameters attribute set as "<a ... [queryParams]="q=|JSON|

As an Angular user, I am trying to populate a query parameter with a JSON object. <ngx-datatable-column name="Sku" prop="product.sku" [flexGrow]="0.5"> <ng-template let-row="row" let-value="value" ...

Can you merge two TypeScript objects with identical keys but different values?

These TypeScript objects have identical keys but different properties. My goal is to merge the properties from one object onto the other. interface Stat<T, V> { name: string; description: string; formatValue: (params: { value: V; item: T }) =&g ...

An issue occurred while trying to run Ionic serve: [ng] Oops! The Angular Compiler is in need of TypeScript version greater than or equal to 4.4.2 and less than 4.5.0, but it seems that version 4

Issue with running the ionic serve command [ng] Error: The Angular Compiler requires TypeScript >=4.4.2 and <4.5.0 but 4.5.2 was found instead. Attempted to downgrade typescript using: npm install typescript@">=4.4.2 <4.5.0" --save-dev --save- ...

Encountered an error while trying to generate the Component class for the ColorlibStepIcon from Material UI in TypeScript

I am trying to convert the ColorlibStepIcon functional component into a class component for my Stepper. Unfortunately, I have not been successful and keep encountering errors. I have attempted some changes but it is still not working as expected. You can ...

What is the best way to find out if an array index is within a certain distance of another index?

I'm currently developing a circular carousel feature. With an array of n items, where n is greater than 6 in my current scenario, I need to identify all items within the array that are either less than or equal to 3 positions away from a specific inde ...

Is it possible for lodash's omit function to return a specific type instead of just a partial type?

Within the following code snippet: import omit from "lodash/fp/omit"; type EnhancerProps = { serializedSvg: string; svgSourceId: string; containerId: string; }; const rest = omit(["serializedSvg", "containerId"])(props); The variable 'rest&a ...

Exploring the Power of Node.JS in Asynchronous Communication

Hey there, I'm not here to talk about async/await or asynchronous programming - I've got that covered. What I really want to know is if it's possible to do something specific within a Node.js Express service. The Situation I've built ...

Discover the Hassle-Free Approach to Triggering Angular Material Menu with ViewChild and MatMenuTrigger

Is there a way to programmatically open an Angular Material menu using a Template Reference Variable on a button trigger that is accessed in the component through ViewChild? I want the menu to open when the mouse hovers over it, instead of just clicking i ...

Resolving the Error: "Type 'Customer | undefined' is not compatible with type 'Customer'" in Angular

I encountered an issue with the following code: ... export class ListCustomersComponent implements OnInit { customers: Array<Customer> = []; showCustomer?: Customer; isSelected: boolean = false; deletedCustomer?: Customer; returnedMessa ...

A keyboard is pressing on tabs and navigating through the app's contents in Ionic 3 on an Android device

I'm currently working on an IONIC 3 app and facing a challenge. When I tap on the ion search and the Keyboard pops up in ANDROID, it disrupts the layout by pushing all the content around. Original screen: https://i.sstatic.net/34iBz.jpg Keyboard m ...

Is the return type determined by the parameter type?

I need to create an interface that can handle different types of parameters from a third-party library, which will determine the return type. The return types could also be complex types or basic types like void or null. Here is a simple example demonstra ...

Dragging element position updated

After implementing a ngFor loop in my component to render multiple CdkDrag elements from an array, I encountered the issue of their positions updating when deleting one element and splicing the array. Is there a way to prevent this unwanted position update ...

Switching the focus of detection from a child to a parent

I am currently working on enhancing the functionality of my UI to display selections dynamically as they are selected or de-selected. import { Wizard } from './report-common'; import { Router } from '@angular/router'; import { DataServ ...

Preventing Redundancy in Angular 2: Tips for Avoiding Duplicate Methods

Is there a way I can streamline my if/else statement to avoid code repetition in my header component? Take a look at the example below: export class HeaderMainComponent { logoAlt = 'We Craft beautiful websites'; // Logo alt and title texts @Vie ...

The async/await feature in Typescript fails to trigger updates in the AngularJS view

Currently, I am utilizing Typescript 2.1 (developer version) to transpile async/await to ES5. An issue I have encountered is that when I modify any property linked to the view within my async function, the view does not automatically reflect the updated v ...

What is the best way to identify if a variable in typescript is null?

Initially, I will perform an HTTP request to a URL in order to retrieve some data. { "data": { "user": { "name": "john", "other": [{ "a": 1, "b": 3 }] } } } My go ...

When attempting to use the 'orderBy' pipe in conjunction with async functions, an error is thrown indicating that the pipe cannot be found

When trying to implement the orderBy pipe in ngFor along with async pipe, I encountered an error as follows: ERROR Error: Uncaught (in promise): Error: Template parse errors: The pipe 'orderBy' could not be found (" </div> ...

Checking the types for object literals returned from Array.map functions

Check out this demonstration I made in the TypeScript playground: interface Test{ a: string b: string } const object: Test = { a: 'b', b: 'c', } function testIt(): Test[] { const data = [{b: '2', c: &apo ...

Generating a new object using an existing one in Typescript

I received a service response containing the following object: let contentArray = { "errorMessages":[ ], "output":[ { "id":1, "excecuteDate":"2022-02-04T13:34:20" ...

Performing operations on an array: What method do you favor and why? Is there a more efficient approach?

What is the most effective method for checking if an element exists in an array? Are there alternative ways to perform a boolean check? type ObjType = { name: string } let privileges: ObjType[] = [{ name: "ROLE_USER" }, { name: "ROLE_ ...