☰ Mục lục series
Tài liệu NestJS tiếng Việt
  1. Nhập môn (Overview)
    1. Giới thiệu
    2. First steps
    3. Controllers
    4. Providers
    5. Modules
    6. Middleware
    7. Exception filters
    8. Pipes
    9. Guards
    10. Interceptors
    11. Custom decorators
  2. Nền tảng (Fundamentals)
    1. Custom providers
    2. Asynchronous providers
    3. Dynamic modules
    4. Injection scopes
    5. Circular dependency
    6. Module reference
    7. Lazy-loading modules
    8. Execution context
    9. Lifecycle events
    10. Discovery service
    11. Platform agnosticism
    12. Testing
  3. Kỹ thuật (Techniques)
    1. Configuration
    2. Database (SQL)
    3. MongoDB
    4. Validation
    5. Caching
    6. Serialization
    7. Versioning
    8. Task scheduling
    9. Queues
    10. Logging
    11. Cookies
    12. Events
    13. Compression
    14. File upload
    15. Streaming files
    16. HTTP module
    17. Session
    18. MVC
    19. Performance (Fastify)
    20. Server-Sent Events
  4. Bảo mật (Security)
    1. Authentication
    2. Authorization
    3. Encryption & Hashing
    4. Helmet
    5. CORS
    6. CSRF
    7. Rate limiting
    8. Vòng đời request
  5. OpenAPI (Swagger)
    1. Introduction
    2. Types and parameters
    3. Operations
    4. Security
    5. Mapped types
    6. Decorators
    7. CLI plugin
    8. Other features
  6. Recipes
    1. REPL
    2. CRUD generator
    3. SWC (fast compiler)
    4. Passport (auth)
    5. Hot reload
    6. MikroORM
    7. Router module
    8. Health checks (Terminus)
    9. CQRS
    10. Prisma
    11. Serve static
    12. Commander
    13. Async local storage
  7. WebSockets
    1. Gateways
    2. Exception filters
    3. Pipes
    4. Guards
    5. Interceptors
    6. Adapters
  8. Microservices
    1. Overview
    2. Redis
    3. MQTT
    4. NATS
    5. RabbitMQ
    6. Kafka
    7. gRPC
    8. Custom transporters
    9. Exception filters
    10. Pipes
    11. Pre-request hooks
    12. Guards
    13. Interceptors
  9. GraphQL
    1. Quick start
    2. Resolvers
    3. Mutations
    4. Subscriptions
    5. Scalars
    6. Directives
    7. Interfaces
    8. Unions and enums
    9. Field middleware
    10. Mapped types
    11. Plugins
    12. Complexity
    13. Extensions
    14. CLI plugin
    15. Generating SDL
    16. Sharing models
    17. Other features
    18. Federation
  10. CLI & triển khai
    1. CLI overview
    2. Workspaces
    3. Libraries
    4. Usage
    5. Scripts
    6. Deployment
    7. Standalone apps
  11. FAQ
    1. Serverless
    2. HTTP adapter
    3. Keep-Alive connections
    4. Global path prefix
    5. Raw body
    6. Hybrid application
    7. HTTPS & multiple servers
    8. Common errors
  12. Phụ lục
    1. Bảng thuật ngữ
Nhập môn (Overview)

Modules

Bản dịch từ docs.nestjs.com/modules

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ả.

Minh hoạ: Modules

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ờ:

src
cats
dto
create-cat.dto.ts
interfaces
cat.interface.ts
cats.controller.ts
cats.module.ts
cats.service.ts
app.module.ts
main.ts

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.

Minh hoạ: Shared Module

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.

Nội dung gốc: NestJS Documentation © Kamil Myśliwiec, giấy phép MIT. Bản dịch và ghi chú: Lê Văn Dũng.