Chuyển đến nội dung chính

Lesson 6: Modules - Organize Code by Feature

Module system in NestJS, Feature modules, Shared modules, Global modules, Dynamic modules. Lazy loading modules and circular dependencies.

💻 Programming — Lesson 6 Lesson 6: Modules - Organize Code by Feature

NestJS: From Basics to Advanced

Part 2: Providers, Dependency Injection & Data Layer

xdev.asia

1. What is a Module?

Module is the unit of code organization in NestJS. Every application has at least one module — root module (AppModule). NestJS encourages code organization accordingly feature modules, each module encapsulates a group of related functions.

@Module({
  imports: [],       // Modules khác mà module này cần
  controllers: [],   // Controllers thuộc module này
  providers: [],     // Services/Providers thuộc module này
  exports: [],       // Providers được chia sẻ ra ngoài
})
export class UsersModule {}

2. Feature Modules

// Cấu trúc thư mục theo feature
src/
├── app.module.ts           // Root module
├── users/
│   ├── users.module.ts
│   ├── users.controller.ts
│   ├── users.service.ts
│   ├── dto/
│   │   ├── create-user.dto.ts
│   │   └── update-user.dto.ts
│   └── entities/
│       └── user.entity.ts
├── products/
│   ├── products.module.ts
│   ├── products.controller.ts
│   ├── products.service.ts
│   └── ...
└── orders/
    ├── orders.module.ts
    └── ...
// users.module.ts
@Module({
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService],  // Cho phép module khác dùng UsersService
})
export class UsersModule {}

// orders.module.ts — cần dùng UsersService
@Module({
  imports: [UsersModule],  // Import để dùng exported providers
  controllers: [OrdersController],
  providers: [OrdersService],
})
export class OrdersModule {}

// app.module.ts — Root module
@Module({
  imports: [UsersModule, ProductsModule, OrdersModule],
})
export class AppModule {}

3. Shared Modules

// Shared module — chứa các utilities dùng chung
@Module({
  providers: [
    HelperService,
    SlugService,
    PaginationService,
  ],
  exports: [
    HelperService,
    SlugService,
    PaginationService,
  ],
})
export class SharedModule {}

// Sử dụng trong nhiều modules
@Module({
  imports: [SharedModule],  // Mỗi module cần import riêng
  // ...
})
export class UsersModule {}

@Module({
  imports: [SharedModule],
  // ...
})
export class ProductsModule {}

4. Global Modules

// Global module — không cần import trong từng module
@Global()
@Module({
  providers: [
    ConfigService,
    LoggerService,
    CacheService,
  ],
  exports: [
    ConfigService,
    LoggerService,
    CacheService,
  ],
})
export class CoreModule {}

// Chỉ cần import một lần trong AppModule
@Module({
  imports: [CoreModule, UsersModule, ProductsModule],
})
export class AppModule {}

// Bây giờ mọi module đều có thể inject ConfigService
// mà KHÔNG cần import CoreModule
@Injectable()
export class UsersService {
  constructor(private config: ConfigService) {} // ✅ Works!
}

⚠️ Warning: Do not abuse @Global(). Only used for truly global services such as Config, Logger, Cache.

5. Dynamic Modules

// Dynamic module — cấu hình tại thời điểm import
@Module({})
export class DatabaseModule {
  static forRoot(options: DatabaseOptions): DynamicModule {
    return {
      module: DatabaseModule,
      global: true,
      providers: [
        { provide: 'DATABASE_OPTIONS', useValue: options },
        {
          provide: 'DATABASE_CONNECTION',
          useFactory: async (opts: DatabaseOptions) => {
            return await createConnection(opts);
          },
          inject: ['DATABASE_OPTIONS'],
        },
        DatabaseService,
      ],
      exports: [DatabaseService, 'DATABASE_CONNECTION'],
    };
  }

  // forFeature — đăng ký entities/repositories
  static forFeature(entities: Type[]): DynamicModule {
    const repositories = entities.map(entity => ({
      provide: getRepositoryToken(entity),
      useFactory: (connection: Connection) => connection.getRepository(entity),
      inject: ['DATABASE_CONNECTION'],
    }));

    return {
      module: DatabaseModule,
      providers: repositories,
      exports: repositories,
    };
  }
}

// Sử dụng
@Module({
  imports: [
    DatabaseModule.forRoot({          // Root config
      host: 'localhost',
      port: 5432,
      database: 'myapp',
    }),
  ],
})
export class AppModule {}

@Module({
  imports: [
    DatabaseModule.forFeature([User, Product]),  // Feature entities
  ],
})
export class UsersModule {}

6. Re-exporting module

@Module({
  imports: [CommonModule],
  exports: [CommonModule],  // Re-export cả module
})
export class CoreModule {}

// Ai import CoreModule sẽ tự động có access CommonModule

7. Handling Circular Dependency

// ❌ Circular: UsersModule ↔ OrdersModule
// UsersModule imports OrdersModule
// OrdersModule imports UsersModule

// ✅ Giải pháp: forwardRef()
@Module({
  imports: [forwardRef(() => OrdersModule)],
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

@Module({
  imports: [forwardRef(() => UsersModule)],
  providers: [OrdersService],
  exports: [OrdersService],
})
export class OrdersModule {}

// Trong service
@Injectable()
export class UsersService {
  constructor(
    @Inject(forwardRef(() => OrdersService))
    private ordersService: OrdersService,
  ) {}
}

Best Practice: Avoid circular dependency. If encountered, separate the common logic into a shared module or use Event-based communication.

8. Summary

  • Feature Modules: Organize code according to business domain
  • Shared Modules: Contains shared utilities, needs import
  • Global Modules: @Global(), automatically available everywhere
  • Dynamic Modules: forRoot() / forFeature() pattern. pattern
  • Avoid circular dependencies, prefer event-based communication

Next post will connect Database with TypeORM and Prisma.