Dependency injection by abstract class in NestJS

Reading Time: 3 minutes

Loading

When developers start using NestJS, they create new module with controllers and services to design backend APIs. Developers register controllers and services in the module and inject service into controller to perform CRUD operations.

Provider Registration

The following is a code snippet of a user module; user controller is registered in controller array while user service is registered in provider array .

@Module({
   controller: [UserController],
   provider: [UserService]
})
export class UserModule {}

[UserService] is concrete class registration and is the short form of

provider: [
  {
     provide: UserService
     useClass: UserService
  }
]

When UserService parameter is seen in the constructor of UserController, an instance of UserService injected.

Abstract Class Registration

At work, the business requirements of our application are to build different type of reports. Each type of report is encapsulated in a module and the controller defines an endpoint that exports report in PDF format and send the attachment to email users.

The naive implementation injects a service and executes functions in controller function to fulfill the purpose. The export function writes contents into a buffer stream and sendEmail attaches it to mail body and sends to some email addresses.

// User Report Module 

@Module({
   controller: [UserReportController],
   provider: [UserReportService]
})
export class UserReportModule {}
// User Report Controller

@Controller('user-report')
export class UserReportController {
   constructor(private service: UserReportService) {}

   @POST()
   async shareReport(): Promise<void> {
       const buffer = await this.service.export()
       await this.service.sendEmail(buffer, 'user1@gmail.com', 'user2@gmail.com')   
   } 
}
// User Report Service

@Injectable()
export class UserReportService {
   async export(): Promise<Buffer> {
    return Buffer.from('Export in user report service')
  }

  sendEmail(buffer: Buffer, ...emails: string[]): void {
    console.log('buffer: ', buffer.toString('utf-8'))
    console.log('send user report to emails:', emails)
  }
}

When there is one type of report, the codes pass our code review process. When the application enhances to support three types of reports; shareReport, export and sendEmail are copied to three modules and DRY principle is violated.

  • It is time to refactor the codes
  • It is time to refactor the codes
  • It is time to refactor the codes

Step 1: Create an abstract report service class in core directory

// Abstract report service

export abstract class AbstractReportService {
  abstract export(): Promise<Buffer>

  sendEmail(buffer: Buffer, ...emails: string[]): void {
    console.log('buffer: ', buffer.toString('utf-8'))
    console.log('send report to emails:', emails)
  }

  async shareReport(...emails: string[]): Promise<void> {
    const buffer = await this.export()
    await this.sendEmail(buffer, ...emails)
  }
}

AbstractReportService class defines shareReport function that exports PDF file and sends the buffer stream to email addresses. sendEmail is reused by different types of reports but export must be implemented by subclasses.

Step 2: Dependency injection by abstract class

Rewrite UserReportModule to provide registration of AbstractReportService

// User Report Module

@Module({
   controller: [UserReportController],
   provider: [
     { 
        provide: AbstractReportService,
        useClass: UserReportService
     }
   ]
})
export class UserReportModule {}
// User Report Controller 

@Controller('user-report')
export class UserReportController {
   constructor(private service: AbstractReportService) {}

   @POST()
   async shareReport(): Promise<void> {
       await this.service.shareReport('user1@gmail.com', 'user2@gmail.com')   
   } 
}
// User Report Service

@Injectable()
export class UserReportService {
   async export(): Promise<Buffer> {
    return Buffer.from('Export in user report service')
  }
}

Step 3: Add new report type after refactoring

When new report type is introduced to the application, the new module registers AbstractReportService and implements the abstract function. Suppose I create PaymentReport module to export a payment report.

// Payment Report Module

@Module({
  controllers: [PaymentReportController],
  providers: [
    {
      provide: AbstractReportService,
      useClass: PaymentReportService,
    },
  ],
})
export class PaymentReportModule {}
// Payment Report Controller

@Controller('payment-report')
export class PaymentReportController {
  constructor(private service: AbstractReportService) {}

  @Post()
  async shareReport(): Promise<void> {
    await this.service.shareReport('john.doe@gmail.com', 'jane.doe@gmail.com')
  }
}
// Payment Report Service

@Injectable()
export class PaymentReportService extends AbstractReportService {
  async export(): Promise<Buffer> {
    return Buffer.from('Export in payment report service')
  }
}

Step 4: CURL POST endpoints

curl -X POST "http://localhost:3000/user-report"
// buffer:  Export in user report service
// send report to emails: [ 'black.doe@gmail.com', 'white.doe@gmail.com' ]

curl -X POST "http://localhost:3000/payment-report"
// buffer:  Export in payment report service
// send report to emails: [ 'john.doe@gmail.com', 'jane.doe@gmail.com' ]

Final thoughts

Dependency injection by abstract class returns an instance of concrete service registered in @Module decorator. This solution has many benefits:

  1. DRY principle because reusable functions are defined in abstract class and subclasses implement module-specific behavior
  2. Shorter controller class since shareReport function is reduced to one line
  3. Write less testing code because shareReport and sendEmail are tested once each. Before refactoring, sendEmail is tested three times.

This is the end of the blog post! I hope you enjoy reading it and NestJS development.

Resources:

  1. Repo: https://github.com/railsstudent/nest-interface-injection
  2. https://dev.to/ef/nestjs-dependency-injection-with-abstract-classes-4g65