NestJS - Providers
Providers là thành phần cơ bản và cực kỳ quan trọng trong Nest để thực hiện Dependency Injection.
Thiết kế kiến trúc Test - Driven Development (TDD) cho dự án với NestJS
Providers
Trong bài viết trước, mình có nhắc đến khái niệm Dependency Injection (DI), đi kèm đó là 3 khái niệm class
Client
Service
Injector
Trong Nest, providers đại diện cho các class Service
. Điều đó có nghĩa là các providers sẽ được inject vào những nơi cần sử dụng thông qua DI. Một số class cơ bản trong Nest được xem là provider như
- Service
- Factory
- Repository
- Helper
- ...
Dependency Injection
Nhờ Typescript, chúng ta có thể dễ dàng quản lý các dependencies trong Nest thông qua kiến trúc type-mapping.
Bạn có thể tham khảo ví dụ sau:
//app.service.ts
import { Injectable } from "@nestjs/common";
@Injectable()
export class AppService {
getHello(): string {
return "Hello World!";
}
}
//app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
Trong AppService
, chúng ta định nghĩa nó là một dependency bằng cách sử dụng decorator @Injectable()
.
Ngoài ra, AppService
được inject vào AppController
thông qua constructor
.
constructor(private readonly appService: AppService) {}
Việc sử dụng Access Modifier (public
- protected
- private
) trong constructor
là một cách viết tắt giúp chúng ta có thể khai báo và khởi tạo property trong class cùng một lúc.
Đoạn code trên tương đương với cách viết như sau:
//app.controller.ts
@Controller()
export class AppController {
private readonly appService: AppService
constructor(appService: AppService) {
this.appService = appService
}
...
}
Một điều quan trọng nữa là các bạn cần register AppService
trong Nest module để nó resolve và thực hiện inject AppService
đúng cách.
Quá trình cũng rất đơn giản. Chúng ta chỉ cần chỉnh sửa file Nest module, sau đó thêm AppService
vào mảng providers
của decorator @Module()
.
//app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Nest cung cấp sẵn một hệ thống IoC Container riêng giúp thực hiện DI một cách dễ dàng. Mình sẽ tóm tắt cơ chế hoạt động của nó như sau
- Trong
app.service.ts
, decorator@Injectable()
khai báo cho IoC Container biếtAppService
là một dependency được quản lý bởi IoC Container. - Trong
app.controller.ts
, controllerAppController
khai báo nó cần dependencyAppService
thông quaconstructor
. - Trong
app.module.ts
, Nest tạo ra một token liên kết vớiAppService
từ tập tinapp.service.ts
. Việc đăng ký được thực hiện thông qua mảngproviders
của decorator@Module()
. - Khi Nest khởi tạo AppController, nó sẽ tìm kiếm tất cả các dependencies mà AppController yêu cầu - ở đây là AppService.
- IoC Container lúc này sẽ kiểm tra tất cả
providers
đã được register trong Nest module và tìmAppService
thông quaAppService
token đã được đăng ký trước đó. - Sau khi tìm thấy, nó sẽ khởi tạo (hoặc lấy về nếu đã tồn tại) instance của
AppService
, sau đó khởi tạo classAppController
, đồng thời injectAppService
vàoAppController
thông quaconstructor
. - Nếu không tìm thấy, Nest sẽ báo lỗi cho bạn biết.
Nest can't resolve dependencies of the AppController (?). Please make sure that the argument AppService at index [0] is available in the AppModule context.
Ngoài ra, IoC Container còn thực hiện analysis các dependencies (Nest gọi nó là tạo Dependency Graph). Dependency Graph bảo đảm các dependencies được resolve theo thứ tự - về cơ bản là "từ dưới lên".
Cơ chế này của IoC Container giúp chúng ta giải quyết vấn đề gặp phải ở bài viết trước - khi bạn có nhiều lớp Service
phụ thuộc nhau.
Providers cơ bản
Đây là cách khai báo đơn giản nhất, có thể sử dụng trong hầu hết các trường hợp.
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
Bạn có thể truyền trực tiếp các class name vào trong mảng providers. Cú pháp này là cách viết tắt của cách viết đầy đủ như sau:
providers: [
{
provide: AppService,
useClass: AppService,
}
]
Nest hỗ trợ một số phương thức để thực hiện resolve dependencies:
- useClass
- useValue
- useFactory
- useExisting
useClass
Bạn chỉ cần khai báo tên class bạn muốn sử dụng làm instance. Hãy nhớ, chỉ có các class đăng ký @Injectable()
mới có thể sử dụng làm instance theo cách này.
useValue
Nest sẽ resolve dependency bằng chính instance hoặc value mà bạn khai báo. Cách này thường được dùng để viết mock test.
import { CatsService } from './cats.service';
const mockCatsService = {
/* mock implementation
...
*/
};
@Module({
imports: [CatsModule],
providers: [
{
provide: CatsService,
useValue: mockCatsService,
},
],
})
export class AppModule {}
useFactory
Cú pháp này giúp chúng ta các provider một cách dynamic. Dependency sẽ được resolve bằng giá trị của hàm useFactory(). Bạn cũng có thể inject dependencies vào các factory phức tạp.
- Bạn có thể truyền các optional params vào factory function
- Nếu truyền params vào factory function, bạn cần khai báo thêm các dependencies cho factory thông qua property
inject
.
const connectionFactory = {
provide: 'CONNECTION',
useFactory: (optionsProvider: OptionsProvider) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider],
};
@Module({
providers: [connectionFactory],
})
export class AppModule {}
useExisting
Phương thức này giúp chúng ta alias các providers đã tồn tại.
@Injectable()
class LoggerService {
/* implementation details */
}
const loggerAliasProvider = {
provide: 'AliasedLoggerService',
useExisting: LoggerService,
};
@Module({
providers: [LoggerService, loggerAliasProvider],
})
export class AppModule {}
Ở ví dụ trên, string token `AliasedLoggerService`
là một alias của LoggerService
. Nếu scope của cả 2 provider này đều là SINGLETON
, thì chúng sẽ trả về cùng một instance.
Non-class provider tokens
Như đã đề cập ở ví dụ trước đó, Nest cũng cho phép bạn register DI tokens bằng string hoặc Symbol
.
@Module({
providers: [
{
provide: 'CAT_REPOSITORY',
useValue: new CatRepository(),
},
],
})
export class AppModule {}
Sau đó sử dụng decorator Inject()
ở nơi cần sử dụng.
@Controller()
export class AppController {
constructor(@Inject('MY_REPOSITORY') private readonly repository: CatRepository {}
...
}
Export Provider
Giả sử bạn có 2 module A và B. Nếu bạn muốn một provider khai báo trong module A có thể được dùng bởi module B, đây là lúc bạn cần export provider đó từ module A, sau đó import module A vào trong module B để sử dụng.
const connectionFactory = {
provide: 'CONNECTION',
useFactory: (optionsProvider: OptionsProvider) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider],
};
@Module({
providers: [connectionFactory],
exports: ['CONNECTION'],
})
export class AppModule {}
Thực ra thì export luôn cái provider object cũng được, chả sao cả.
@Module({
providers: [connectionFactory],
exports: [connectionFactory],
})
export class AppModule {}
Kết
Mình xin phép kết thúc bài viết ở đây. Hẹn gặp lại trong phần tiếp theo nhé.