Module là một class được đánh dấu bằng decorator @Module(). Decorator này cung cấp metadata mà Nest dùng để tổ chức và quản lý cấu trúc ứng dụng một cách hiệu quả.

Mọi ứng dụng Nest đều có ít nhất một module, đó là root module, đóng vai trò là điểm khởi đầu để Nest xây dựng application graph. Graph này là một cấu trúc nội bộ mà Nest dùng để resolve các mối quan hệ và dependency giữa các module và provider. Dù các ứng dụng nhỏ có thể chỉ có một root module, nhưng thông thường không phải vậy. Module được khuyến khích mạnh mẽ như một cách hiệu quả để tổ chức các thành phần của bạn. Với hầu hết ứng dụng, bạn sẽ có nhiều module, mỗi module đóng gói một tập hợp capability (khả năng) liên quan chặt chẽ với nhau.
Decorator @Module() nhận một object duy nhất với các property mô tả module:
providers |
các provider sẽ được Nest injector khởi tạo và có thể được chia sẻ ít nhất trong phạm vi module này |
controllers |
tập hợp các controller được định nghĩa trong module này và cần được khởi tạo |
imports |
danh sách các module được import, những module này export các provider mà module hiện tại cần |
exports |
tập con của providers được cung cấp bởi module này và nên khả dụng ở các module khác import module này. Bạn có thể dùng chính provider đó hoặc chỉ token của nó (giá trị provide) |
Module đóng gói (encapsulate) provider theo mặc định, nghĩa là bạn chỉ có thể inject những provider thuộc module hiện tại hoặc được export tường minh từ các module khác đã import. Các provider được export từ một module về cơ bản đóng vai trò như public interface hay API của module đó.
Ghi chú người dịch Cơ chế này khác với require/import thông thường trong Node.js (hay Express) - ở đó bạn import một file là dùng được ngay, không có khái niệm “phải export mới lộ ra ngoài”. Nó gần với package ở Go hơn: chỉ những identifier viết hoa (exported) mới dùng được từ package khác, còn lại bị giới hạn trong phạm vi package.
Feature modules
Trong ví dụ của chúng ta, CatsController và CatsService có liên quan chặt chẽ với nhau và phục vụ cùng một domain của ứng dụng. Việc nhóm chúng vào một feature module là hợp lý. Feature module tổ chức code liên quan đến một tính năng cụ thể, giúp duy trì ranh giới rõ ràng và tổ chức tốt hơn. Điều này đặc biệt quan trọng khi ứng dụng hoặc team phát triển lớn hơn, và nó phù hợp với nguyên lý SOLID.
Tiếp theo, chúng ta sẽ tạo CatsModule để minh họa cách nhóm controller và service lại với nhau.
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller.js';
import { CatsService } from './cats.service.js';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}
Gợi ý Để tạo một module bằng CLI, chỉ cần chạy lệnh $ nest g module cats.
Ở trên, chúng ta đã định nghĩa CatsModule trong file cats.module.ts, và chuyển mọi thứ liên quan đến module này vào thư mục cats. Việc cuối cùng cần làm là import module này vào root module (AppModule, được định nghĩa trong file app.module.ts).
import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module.js';
@Module({
imports: [CatsModule],
})
export class AppModule {}
Đây là cấu trúc thư mục của chúng ta bây giờ:
Shared modules
Trong Nest, module là singleton theo mặc định, vì vậy bạn có thể chia sẻ cùng một instance của bất kỳ provider nào giữa nhiều module một cách dễ dàng.

Mọi module đều tự động là một shared module (module chia sẻ). Sau khi được tạo, nó có thể được tái sử dụng bởi bất kỳ module nào. Giả sử chúng ta muốn chia sẻ một instance của CatsService giữa nhiều module khác. Để làm điều đó, trước tiên ta cần export provider CatsService bằng cách thêm nó vào mảng exports của module, như dưới đây:
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller.js';
import { CatsService } from './cats.service.js';
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService]
})
export class CatsModule {}
Giờ đây, bất kỳ module nào import CatsModule đều có thể truy cập CatsService và sẽ chia sẻ cùng một instance với tất cả các module khác cũng import nó.
Nếu chúng ta đăng ký trực tiếp CatsService trong mọi module cần nó, điều đó vẫn hoạt động, nhưng sẽ khiến mỗi module có một instance riêng biệt của CatsService. Điều này có thể dẫn đến việc tăng mức sử dụng bộ nhớ vì nhiều instance của cùng một service được tạo ra, và cũng có thể gây ra hành vi không mong muốn, chẳng hạn như trạng thái không nhất quán (state inconsistency) nếu service đó duy trì bất kỳ internal state nào.
Bằng cách đóng gói CatsService bên trong một module, như CatsModule, và export nó, chúng ta đảm bảo rằng cùng một instance của CatsService được tái sử dụng trên tất cả các module import CatsModule. Điều này không chỉ giảm mức tiêu thụ bộ nhớ mà còn mang lại hành vi dễ dự đoán hơn, vì tất cả module chia sẻ cùng một instance, giúp việc quản lý state hoặc resource dùng chung dễ dàng hơn. Đây là một trong những lợi ích chính của tính module hóa (modularity) và dependency injection trong các framework như NestJS - cho phép service được chia sẻ hiệu quả trong toàn bộ ứng dụng.
Ghi chú người dịch Cạm bẫy hay gặp khi vibe-code: nếu bạn khai báo cùng một provider trong providers của nhiều module khác nhau (thay vì export/import), mỗi module sẽ có instance riêng - service tưởng là “singleton toàn app” nhưng lại giữ state khác nhau ở từng nơi, dẫn tới bug rất khó debug (ví dụ cache, counter, hoặc connection pool không đồng bộ).
Module re-exporting
Như đã thấy ở trên, module có thể export các provider nội bộ của nó. Ngoài ra, chúng còn có thể re-export các module mà chúng import. Trong ví dụ dưới đây, CommonModule vừa được import vừa được export từ CoreModule, khiến nó khả dụng cho các module khác import CoreModule này.
@Module({
imports: [CommonModule],
exports: [CommonModule],
})
export class CoreModule {}
Tiêm phụ thuộc (Dependency injection)
Một module class cũng có thể inject provider (ví dụ, cho mục đích cấu hình):
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller.js';
import { CatsService } from './cats.service.js';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {
constructor(private catsService: CatsService) {}
}
import { Module, Dependencies } from '@nestjs/common';
import { CatsController } from './cats.controller.js';
import { CatsService } from './cats.service.js';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
@Dependencies(CatsService)
export class CatsModule {
constructor(catsService) {
this.catsService = catsService;
}
}
Tuy nhiên, bản thân các module class không thể được inject như provider do phụ thuộc vòng (circular dependency).
Global modules
Nếu bạn phải import cùng một tập hợp module ở khắp mọi nơi, điều đó có thể trở nên rườm rà. Không giống Nest, providers trong Angular được đăng ký ở global scope. Một khi đã định nghĩa, chúng khả dụng ở mọi nơi. Tuy nhiên, Nest đóng gói provider bên trong phạm vi module. Bạn không thể dùng provider của một module ở nơi khác nếu không import module đóng gói (encapsulating module) đó trước.
Khi bạn muốn cung cấp một tập hợp provider nên khả dụng ở mọi nơi ngay từ đầu (ví dụ, helper, kết nối database, v.v.), hãy biến module đó thành global bằng decorator @Global().
import { Module, Global } from '@nestjs/common';
import { CatsController } from './cats.controller.js';
import { CatsService } from './cats.service.js';
@Global()
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService],
})
export class CatsModule {}
Decorator @Global() khiến module có phạm vi global. Global module chỉ nên được đăng ký một lần duy nhất, thường là bởi root module hoặc core module. Trong ví dụ trên, provider CatsService sẽ có mặt ở khắp mọi nơi, và các module muốn inject service này sẽ không cần import CatsModule vào mảng imports của chúng.
Ghi chú người dịch Tương tự việc bind một service vào service container global trong Laravel (app()->singleton(...)) - dùng được ở đâu cũng tiện, nhưng code đọc vào một module sẽ không còn thấy rõ nó phụ thuộc vào đâu (không có dòng imports nào để tra ngược), nên càng dễ tạo coupling ngầm giữa các phần không liên quan.
Gợi ý Việc biến mọi thứ thành global không được khuyến khích như một cách thiết kế. Dù global module có thể giúp giảm boilerplate, nhưng nhìn chung dùng mảng imports để cung cấp API của module cho các module khác một cách có kiểm soát và rõ ràng sẽ tốt hơn. Cách tiếp cận này mang lại cấu trúc và khả năng bảo trì tốt hơn, đảm bảo chỉ những phần cần thiết của module được chia sẻ với module khác trong khi tránh sự coupling không cần thiết giữa các phần không liên quan của ứng dụng.
Dynamic modules
Dynamic module trong Nest cho phép bạn tạo các module có thể được cấu hình tại runtime. Điều này đặc biệt hữu ích khi bạn cần cung cấp các module linh hoạt, có thể tùy chỉnh, nơi provider được tạo ra dựa trên một số option hoặc cấu hình nhất định. Dưới đây là tổng quan ngắn gọn về cách dynamic module hoạt động.
import { Module, DynamicModule } from '@nestjs/common';
import { createDatabaseProviders } from './database.providers.js';
import { Connection } from './connection.provider.js';
@Module({
providers: [Connection],
exports: [Connection],
})
export class DatabaseModule {
static forRoot(entities = [], options?): DynamicModule {
const providers = createDatabaseProviders(options, entities);
return {
module: DatabaseModule,
providers: providers,
exports: providers,
};
}
}
import { Module } from '@nestjs/common';
import { createDatabaseProviders } from './database.providers.js';
import { Connection } from './connection.provider.js';
@Module({
providers: [Connection],
exports: [Connection],
})
export class DatabaseModule {
static forRoot(entities = [], options) {
const providers = createDatabaseProviders(options, entities);
return {
module: DatabaseModule,
providers: providers,
exports: providers,
};
}
}
Gợi ý Method forRoot() có thể trả về một dynamic module theo cách đồng bộ hoặc bất đồng bộ (tức là qua một Promise).
Module này định nghĩa provider Connection theo mặc định (trong metadata của decorator @Module()), nhưng đồng thời - tùy vào các object entities và options được truyền vào method forRoot() - còn expose (lộ ra) thêm một tập hợp provider, ví dụ như repository. Lưu ý rằng các property được trả về bởi dynamic module sẽ mở rộng (extend) (thay vì ghi đè) metadata gốc của module đã định nghĩa trong decorator @Module(). Đó là cách mà cả provider Connection được khai báo tĩnh và các repository provider được sinh ra động đều được export từ module.
Nếu muốn đăng ký một dynamic module ở phạm vi global, hãy đặt property global thành true.
{
global: true,
module: DatabaseModule,
providers: providers,
exports: providers,
}
Cảnh báo Như đã đề cập ở trên, việc biến mọi thứ thành global không phải là một quyết định thiết kế tốt.
DatabaseModule có thể được import và cấu hình theo cách sau:
import { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module.js';
import { User } from './users/entities/user.entity.js';
@Module({
imports: [DatabaseModule.forRoot([User])],
})
export class AppModule {}
Nếu muốn re-export ngược lại một dynamic module, bạn có thể bỏ qua lời gọi method forRoot() trong mảng exports:
import { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module.js';
import { User } from './users/entities/user.entity.js';
@Module({
imports: [DatabaseModule.forRoot([User])],
exports: [DatabaseModule],
})
export class AppModule {}
Chương Dynamic modules sẽ trình bày chủ đề này chi tiết hơn, và bao gồm một ví dụ hoạt động thực tế.
Gợi ý Tìm hiểu cách xây dựng dynamic module có khả năng tùy chỉnh cao với ConfigurableModuleBuilder tại chương này.