Back to blog

NestJS Modules Giải Thích

nestjsnodejstypescriptbackend
NestJS Modules Giải Thích

NestJS học rất nhiều từ architecture của Angular, và modules là building block nền tảng của nó. Mọi NestJS application đều có ít nhất một module — root module — và trong thực tế bạn sẽ có rất nhiều, mỗi cái đóng gói một domain riêng biệt của ứng dụng.

Module Là Gì?

Module là một class được annotate bằng @Module() decorator. Nó nói với NestJS cách lắp ráp application graph — những provider nào tồn tại, controller nào nằm trong module này, và module này expose gì ra bên ngoài.

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
 
@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}

@Module() decorator nhận một metadata object duy nhất với bốn optional property:

PropertyMô tả
providersServices và các injectable khác sẽ được NestJS injector khởi tạo
controllersControllers được khởi tạo trong module này
importsCác module khác mà module này cần dùng exported providers của chúng
exportsProviders từ module này mà các module khác có thể import

Root Module

Mọi app bắt đầu từ root module, được truyền vào NestFactory.create():

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
 
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

Root module (AppModule) chỉ là một module bình thường — theo convention nó nằm trong app.module.ts và import tất cả feature modules.

Feature Modules

Khi ứng dụng lớn dần, bạn chia domain logic thành các feature modules. Một cats feature, ví dụ, gom tất cả code liên quan đến cats lại:

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

Feature module khai báo controller và provider của riêng nó:

// cats/cats.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
 
@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}

Rồi root module import nó:

// app.module.ts
import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';
 
@Module({
  imports: [CatsModule],
})
export class AppModule {}

CatsService giờ được scoped vào CatsModule. Không gì bên ngoài có thể inject nó trừ khi CatsModule export nó ra một cách tường minh.

Shared Modules

Trong NestJS, modules là singleton theo mặc định. Khi một module được khởi tạo, instance đó được chia sẻ cho tất cả các module import nó. Đây là điều làm NestJS efficient và predictable.

Để provider có thể dùng được ở các module khác, export nó ra:

@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService],  // giờ các module khác có thể dùng CatsService
})
export class CatsModule {}

Bất kỳ module nào import CatsModule đều nhận cùng một CatsService instance:

@Module({
  imports: [CatsModule],  // có quyền truy cập CatsService
  providers: [DogsService],
  controllers: [DogsController],
})
export class DogsModule {}
// dogs.service.ts — giờ có thể inject CatsService
@Injectable()
export class DogsService {
  constructor(private catsService: CatsService) {}
}

Module Re-exporting

Một module có thể re-export các module mà nó import. Cái này rất hữu ích khi tạo "barrel" modules gom chung các functionality liên quan:

@Module({
  imports: [CommonModule],
  exports: [CommonModule],  // re-export để ai import CoreModule cũng được CommonModule
})
export class CoreModule {}

Giờ bất kỳ module nào import CoreModule tự động được tất cả những gì CommonModule export, không cần import trực tiếp.

Global Modules

Đôi khi bạn có các utility (database connection, config service, logger) mà mọi module đều cần. Import chúng ở khắp nơi thì rất phiền. Mark chúng với @Global():

import { Global, Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
 
@Global()
@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService],
})
export class CatsModule {}

Global module chỉ cần đăng ký một lần (thường là trong root module). Sau đó, bất kỳ module nào cũng có thể inject các exported providers của nó mà không cần import module đó.

Lưu ý: Đừng lạm dụng @Global(). Nó bypass dependency graph tường minh — chính cái làm NestJS apps dễ maintain. Dùng ít thôi — config, database, và logging là những candidate hợp lý.

Dynamic Modules

Static modules cover hầu hết các case, nhưng đôi khi bạn cần configure một module tại runtime — ví dụ database connection string hay API key được truyền qua environment variables.

Dynamic modules cho phép một module tự return metadata của mình, tính toán tại thời điểm đăng ký:

import { Module, DynamicModule } from '@nestjs/common';
import { DatabaseService } from './database.service';
 
@Module({})
export class DatabaseModule {
  static forRoot(uri: string): DynamicModule {
    return {
      module: DatabaseModule,
      providers: [
        {
          provide: 'DATABASE_URI',
          useValue: uri,
        },
        DatabaseService,
      ],
      exports: [DatabaseService],
    };
  }
}

Dùng trong AppModule:

@Module({
  imports: [DatabaseModule.forRoot('mongodb://localhost/mydb')],
})
export class AppModule {}

Convention là forRoot() cho singleton configuration (dùng một lần trong root) và forFeature() cho module-level configuration (dùng trong feature modules, như TypeORM's forFeature([UserEntity])).

Làm Dynamic Modules Có Thể Dùng Async Config

Nhiều NestJS ecosystem packages (TypeORM, Config, JWT) expose thêm forRootAsync() để factory function có thể inject các service khác:

static forRootAsync(options: DatabaseOptions): DynamicModule {
  return {
    module: DatabaseModule,
    imports: options.imports || [],
    providers: [
      {
        provide: 'DATABASE_OPTIONS',
        useFactory: options.useFactory,
        inject: options.inject || [],
      },
      DatabaseService,
    ],
    exports: [DatabaseService],
  };
}

forRootAsync() defer việc tạo provider sang một factory function, nghĩa là bạn có thể inject ConfigService hay các async value khác:

DatabaseModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: (config: ConfigService) => ({
    uri: config.get('DB_URI'),
  }),
  inject: [ConfigService],
}),

Tổng Quan Module Lifecycle

Đây là cách modules hoạt động ở runtime:

Các điểm chính:

  • NestJS build một dependency graph lúc startup, khởi tạo modules và providers theo đúng thứ tự
  • Providers là singleton trong scope module của chúng
  • Shared modules chia sẻ cùng một instance cho tất cả module import
  • Global modules bỏ qua yêu cầu import hoàn toàn

Ví Dụ Thực Tế: User Feature Module

Một feature module thực tế cho user management:

// users/users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';
 
@Module({
  imports: [TypeOrmModule.forFeature([User])],  // dynamic module cho feature này
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService],  // AuthModule sẽ cần cái này
})
export class UsersModule {}
// auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { UsersModule } from '../users/users.module';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
 
@Module({
  imports: [
    UsersModule,  // import UsersService
    JwtModule.registerAsync({
      useFactory: (config: ConfigService) => ({
        secret: config.get('JWT_SECRET'),
        signOptions: { expiresIn: '7d' },
      }),
      inject: [ConfigService],
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService],
})
export class AuthModule {}
// app.module.ts
@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),  // global config
    TypeOrmModule.forRootAsync({ ... }),         // global database
    UsersModule,
    AuthModule,
  ],
})
export class AppModule {}

Structure này giữ cho mỗi domain tự chứa: UsersModule sở hữu toàn bộ user logic, AuthModule kết hợp với JWT capability, và AppModule kết nối tất cả lại.

Tóm Tắt

✅ Mọi NestJS app có một root module import tất cả feature modules
✅ Feature modules đóng gói controllers và providers cho một domain duy nhất
✅ exports kiểm soát những gì một module chia sẻ ra bên ngoài
✅ Modules là singleton — cùng một instance được chia sẻ cho tất cả module import
✅ @Global() làm exported providers available everywhere không cần import tường minh
✅ Dynamic modules (forRoot() / forFeature()) cho phép runtime configuration
✅ forRootAsync() hỗ trợ async config thông qua factory functions và injected services

Hiểu modules là chìa khóa để hiểu NestJS architecture. Mọi thứ — guards, interceptors, pipes, TypeORM repositories, JWT — đều plug in thông qua module system.

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.