Provider là một khái niệm cốt lõi trong Nest. Rất nhiều class cơ bản của Nest, như service, repository, factory, và helper, đều có thể được coi là provider. Ý tưởng chính đằng sau provider là nó có thể được inject như một dependency, cho phép các object hình thành nhiều mối quan hệ khác nhau với nhau. Trách nhiệm “nối” (wiring up) các object này phần lớn được đảm nhiệm bởi hệ thống runtime của Nest.

Ở chương trước, chúng ta đã tạo một CatsController đơn giản. Controller nên xử lý HTTP request và giao các tác vụ phức tạp hơn cho provider. Provider là các class JavaScript thuần được khai báo trong providers của một module NestJS. Để biết thêm chi tiết, tham khảo chương “Modules”.
Gợi ý Vì Nest cho phép bạn thiết kế và tổ chức dependency theo hướng object-oriented, chúng tôi khuyến khích mạnh mẽ việc tuân theo nguyên lý SOLID.
Service (Services)
Hãy bắt đầu bằng việc tạo một CatsService đơn giản. Service này sẽ xử lý việc lưu trữ và truy xuất dữ liệu, và sẽ được CatsController sử dụng. Vì vai trò của nó trong việc quản lý logic của ứng dụng, đây là ứng viên lý tưởng để định nghĩa như một provider.
import { Injectable } from '@nestjs/common';
import type { Cat } from './interfaces/cat.interface.js';
@Injectable()
export class CatsService {
private readonly cats: Cat[] = [];
create(cat: Cat) {
this.cats.push(cat);
}
findAll(): Cat[] {
return this.cats;
}
}
import { Injectable } from '@nestjs/common';
@Injectable()
export class CatsService {
constructor() {
this.cats = [];
}
create(cat) {
this.cats.push(cat);
}
findAll() {
return this.cats;
}
}
Gợi ý Để tạo một service bằng CLI, chỉ cần chạy lệnh $ nest g service cats.
CatsService của chúng ta là một class cơ bản với một property và hai method. Điểm bổ sung quan trọng ở đây là decorator @Injectable(). Decorator này gắn metadata vào class, báo hiệu rằng CatsService là một class có thể được quản lý bởi IoC container của Nest.
Ghi chú người dịch Nếu ví với React, @Injectable() giống việc “đăng ký” một giá trị để đưa vào Context, nhưng khác biệt quan trọng: Nest tự tìm và truyền instance dựa theo type khai báo ở constructor, bạn không cần bọc <Provider> hay gọi useContext() ở nơi dùng - đổi lại, type đó phải là một class thật (xem cảnh báo ở phần Dependency injection bên dưới) để Nest tra cứu được lúc runtime.
Ngoài ra, ví dụ này còn sử dụng một interface Cat, có dạng như sau:
export interface Cat {
name: string;
age: number;
breed: string;
}
Giờ đã có một service class để truy xuất cats, hãy sử dụng nó bên trong CatsController:
import { Controller, Get, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto.js';
import { CatsService } from './cats.service.js';
import type { Cat } from './interfaces/cat.interface.js';
@Controller('cats')
export class CatsController {
constructor(private catsService: CatsService) {}
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
}
import { Controller, Get, Post, Body, Bind, Dependencies } from '@nestjs/common';
import { CatsService } from './cats.service.js';
@Controller('cats')
@Dependencies(CatsService)
export class CatsController {
constructor(catsService) {
this.catsService = catsService;
}
@Post()
@Bind(Body())
async create(createCatDto) {
this.catsService.create(createCatDto);
}
@Get()
async findAll() {
return this.catsService.findAll();
}
}
CatsService được inject thông qua constructor của class. Chú ý từ khóa private: đây là cú pháp rút gọn của TypeScript, khai báo và khởi tạo member catsService ngay trên cùng một dòng. Phần tiếp theo sẽ xem xét kỹ hơn việc Nest xử lý khai báo đó như thế nào.
Tiêm phụ thuộc (Dependency injection)
Nest được xây dựng dựa trên design pattern mạnh mẽ có tên Dependency Injection. Chúng tôi khuyến khích bạn đọc bài viết rất hay về khái niệm này trong tài liệu chính thức của Angular.
Trong Nest, nhờ khả năng của TypeScript, việc quản lý dependency trở nên đơn giản vì chúng được resolve dựa trên type. Trong ví dụ dưới đây, Nest sẽ resolve catsService bằng cách tạo và trả về một instance của CatsService (hoặc, trong trường hợp singleton, trả về instance đã tồn tại nếu nó đã được yêu cầu ở nơi khác trước đó). Dependency này sau đó được inject vào constructor của controller (hoặc được gán vào property được chỉ định):
constructor(private catsService: CatsService) {}
Có hai điều đang diễn ra trong dòng code đó. Từ khóa private là một parameter property của TypeScript: nó khai báo một member catsService trên class và gán tham số constructor cho nó, nên bạn không cần tự viết this.catsService = catsService. Type annotation CatsService mới thực sự là thứ Nest dùng để resolve - tại compile time, TypeScript phát ra (emit) type của các tham số constructor dưới dạng metadata, và container đọc metadata đó để xác định provider nào cần cung cấp.
Cảnh báo Vì việc resolve dựa trên type được emit ra, annotation đó phải là thứ vẫn tồn tại tại runtime - nghĩa là một class. Một interface hay type alias sẽ bị xóa (erased) trong quá trình biên dịch, nên constructor(private config: AppConfig) với AppConfig là một interface sẽ khiến Nest không có gì để tra cứu, và bạn sẽ gặp lỗi “Nest can’t resolve dependencies” khi khởi động. Khi cần inject thứ gì đó không phải class, hãy gán cho nó một token và inject tường minh bằng @Inject() - xem Custom providers.
Phạm vi (Scopes)
Provider thường có một vòng đời (“scope”) gắn liền với vòng đời của ứng dụng. Khi ứng dụng được bootstrap, mỗi dependency phải được resolve, nghĩa là mọi provider đều được khởi tạo. Tương tự, khi ứng dụng shutdown, tất cả provider đều bị hủy. Tuy nhiên, cũng có thể làm cho một provider request-scoped (theo phạm vi request), nghĩa là vòng đời của nó gắn với một request cụ thể thay vì vòng đời của ứng dụng. Bạn có thể tìm hiểu thêm về các kỹ thuật này trong chương Injection Scopes.
Ghi chú người dịch Cân nhắc kỹ trước khi bật request-scoped: mỗi request sẽ tạo lại toàn bộ chain provider phụ thuộc vào nó (không chỉ provider đó, mà mọi provider “tiêu thụ” nó cũng bị nâng lên request-scoped theo), tương tự việc bind một service theo instance mới mỗi lần resolve trong Laravel container thay vì singleton - đổi lại một chút overhead để có được state riêng theo từng request.
Custom providers
Nest đi kèm một IoC container (“Inversion of Control”) có sẵn để quản lý các mối quan hệ giữa provider. Tính năng này là nền tảng của dependency injection, nhưng thực ra nó mạnh mẽ hơn nhiều so với những gì đã đề cập. Có nhiều cách để định nghĩa một provider: bạn có thể dùng giá trị thuần (plain value), class, và factory bất đồng bộ (asynchronous) hoặc đồng bộ (synchronous). Để xem thêm ví dụ về việc định nghĩa provider, hãy tham khảo chương Dependency Injection.
Optional providers
Đôi khi, bạn có thể có những dependency không nhất thiết phải luôn được resolve. Ví dụ, class của bạn có thể phụ thuộc vào một configuration object, nhưng nếu không có object nào được cung cấp, giá trị mặc định sẽ được dùng. Trong trường hợp này, dependency được coi là optional (tùy chọn), và việc thiếu configuration provider không nên gây ra lỗi.
Để đánh dấu một provider là optional, dùng decorator @Optional() trong signature của constructor.
import { Injectable, Optional, Inject } from '@nestjs/common';
@Injectable()
export class HttpService<T> {
constructor(@Optional() @Inject('HTTP_OPTIONS') private httpClient: T) {}
}
Trong ví dụ trên, chúng ta đang dùng một custom provider, đó là lý do vì sao ta phải kèm theo token tùy chỉnh HTTP_OPTIONS. Các ví dụ trước đã minh họa constructor-based injection, trong đó dependency được chỉ định thông qua một class trong constructor. Để biết thêm chi tiết về custom provider và cách token liên kết với chúng hoạt động, hãy xem chương Custom Providers.
Lưu ý rằng @Optional() chỉ thay đổi điều gì xảy ra khi provider bị thiếu, chứ không thay đổi những gì bạn nhận được khi nó tồn tại: nếu không có gì được đăng ký dưới HTTP_OPTIONS, Nest sẽ inject undefined thay vì báo lỗi khi khởi động. Điều đó nghĩa là class phải tự chịu trách nhiệm xử lý fallback - thường là bằng cách merge giá trị được truyền vào lên trên một tập giá trị mặc định, thay vì giả định rằng giá trị đó luôn tồn tại.
Property-based injection
Kỹ thuật chúng ta đã dùng cho đến giờ được gọi là constructor-based injection, trong đó provider được inject thông qua constructor. Trong một số trường hợp cụ thể, property-based injection có thể hữu ích. Ví dụ, nếu class ở tầng trên cùng của bạn phụ thuộc vào một hoặc nhiều provider, việc truyền tất cả chúng qua super() trong các sub-class có thể trở nên cồng kềnh. Để tránh điều này, bạn có thể dùng decorator @Inject() trực tiếp ở cấp property.
import { Injectable, Inject } from '@nestjs/common';
@Injectable()
export class HttpService<T> {
@Inject('HTTP_OPTIONS')
private readonly httpClient: T;
}
Cảnh báo Nếu class của bạn không extend một class khác, nhìn chung dùng constructor-based injection sẽ tốt hơn. Constructor thể hiện rõ ràng những dependency nào là bắt buộc, mang lại khả năng quan sát tốt hơn và giúp code dễ hiểu hơn so với các property được đánh dấu bằng @Inject.
Ghi chú người dịch Nguyên tắc này cũng phổ biến ở Go: dependency truyền qua constructor (ví dụ NewService(repo Repo)) luôn được ưu tiên hơn gán trực tiếp vào field, vì nhìn signature là biết ngay service cần gì để hoạt động, và khi viết test bạn truyền mock vào ngay mà không cần đụng tới implementation nội bộ.
Đăng ký provider (Provider registration)
Giờ đã định nghĩa xong một provider (CatsService) và một consumer (CatsController), chúng ta cần đăng ký service này với Nest để nó có thể xử lý việc injection. Việc này được thực hiện bằng cách chỉnh sửa file module (app.module.ts) và thêm service vào mảng providers trong decorator @Module().
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller.js';
import { CatsService } from './cats/cats.service.js';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class AppModule {}
Giờ Nest đã có thể resolve các dependency của class CatsController.
Đến đây, cấu trúc thư mục của chúng ta trông như sau:
Khởi tạo thủ công (Manual instantiation)
Cho đến giờ, chúng ta đã tìm hiểu cách Nest tự động xử lý phần lớn chi tiết của việc resolve dependency. Tuy nhiên, trong một số trường hợp, bạn có thể cần đi ra ngoài hệ thống Dependency Injection có sẵn để thủ công lấy về hoặc khởi tạo provider. Hai kỹ thuật như vậy được trình bày ngắn gọn dưới đây.
- Để lấy về các instance đã tồn tại hoặc khởi tạo provider một cách linh động, bạn có thể dùng Module reference.
- Để lấy provider bên trong hàm
bootstrap()(ví dụ, cho ứng dụng standalone hoặc để dùng một configuration service trong quá trình bootstrap), xem Standalone applications.