Exploring module in NestJS

Published on
6 mins read
--- views

Introduction

Modules are crucial components at the center of NestJS. In this post, we'll take a closer look at what modules are in NestJS.

A module is a class annotated with a @Module() decorator. The @Module() decorator provides metadata that Nest makes use of to organize the application structure:

  • Each application has at least one module, a root module.
  • In NestJS modules are strongly recommended as an effective way to organize your components.
  • Each module will encapsulate a set of closely related functions.
Diagram Module In NestJS

The decorator @Module() takes a single object whose properties describe the module:

modules/product/product.module.ts
import { Module } from '@nestjs/common';

import CategoryModule from 'modules/category.module'; // Expose route controller.

import ProductController from './product.controller'; // Expose route controller.
import ProductService from './product.service';       // Business Logic
import ProductRepository from './product.repository'; // Data Access

@Module({
  imports: [CategoryModule],
  controllers: [ProductsController],
  providers: [ProductService, ProductRepository],
  exports: [ProductService],
});

export class ProductModule {}
  • provider: The Nest injector will create providers that can be shared within this module at a minimum.
  • controllers: The set of controllers defined in this module
  • imports: The list of imported modules that export the providers
  • exports: The subset of providers that are provided by this module and should be available in other modules which import this module.

Types Of Modules In NestJS

Feature Module

  • A feature module simply organizes code relevant for a specific feature, keeping code organized and establishing clear boundaries.

  • To demonstrate this, we'll create the ProductModule (responsible for encapsulating Product logic and endpoints).

modules/product/product.module.ts
import { Module } from '@nestjs/common';

import ProductController from './product.controller';
import ProductService from './product.service';

@Module({
  controllers: [ProductController],
  providers: [ProductService],
})
export default class ProductModule {}
  • The last thing we need to do is import this module into the root module (the AppModule, defined in the app.module.ts file).
app.module.ts
import { Module } from '@nestjs/common';

import ProductModule from './product/product.module';

@Module({
  imports: [ProductModule],
})
export class AppModule {}

Shared Modules

  • In Nest, modules are singletons (design patterns) by default, and thus you can share the same instance of any provider between multiple modules effortlessly.

  • Every module is automatically a shared module.

Diagram Shared Module In NestJS
core/logger/logger.module.ts
import { Module } from '@nestjs/common';

import LoggerService from './logger.service';

@Module({
  providers: [LoggerService],
  exports: [LoggerService],
})
export default class LoggerModule {}
modules/product/product.module.ts
import { Module } from '@nestjs/common';

import LoggerModule from 'core/logger/logger.module';

import ProductController from './product.controller';
import ProductService from './product.service';

@Module({
  imports: [LoggerModule],
  controllers: [ProductController],
  providers: [ProductService],
})
export class ProductModule {}

// Import similar LoggerModule for ShopMonitoringModule and PriceMonitoringModule.

Global Modules

  • In the showcased example within the shared module section, the LoggerModule undergoes multiple imports across three distinct modules—namely, ProductModule, ShopMonitoringModule, and PriceMonitoringModule.

  • This redundancy is both noticeable and avoidable. To address this issue, Nest offers a feature known as the Global Module.

  • Rather than importing the LoggerModule in each module requiring its functionality, we can simplify the process by importing the LoggerModule just once in the AppModule (the root module) and applying the @Global() decorator to the class, turning it into a Global Module.

  • This approach significantly reduces redundancy and promotes a more efficient and maintainable code structure.

Diagram Global Module In NestJS
  • @Global() decorator make the module global, that you want to provide a set of providers which should be available everywhere:
core/logger/logger.module.ts
import { Module, Global } from '@nestjs/common';

import LoggerService from './logger.service';

@Global()
@Module({
  providers: [LoggerService],
  exports: [LoggerService],
})
export class LoggerModule {}
  • Import LoggerModule into AppModule (root module):
app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';

import { LoggerModule } from './core/logger/logger.module';
import { ProductModule } from './modules/product/product.module';
import { ShopMonitoringModule } from './modules/shop-monitoring/shop-monitoring.module';
import { PriceMonitoringModule } from './modules/price-monitoring/price-monitoring.module';

@Module({
  imports: [ProductModule, ShopMonitoringModule, PriceMonitoringModule, LoggerModule],
  controllers: [],
  providers: [],
})
export class AppModule {}
  • Using LoggerService in ProductService (We do not need to import LoggerModule into ProductModule):
modules/product/product.service.ts
import { Injectable } from '@nestjs/common';

import { LoggerService } from '../../core/logger/logger.service';

@Injectable()
export class ProductService {
  constructor(private logger: LoggerService) {}

  async findAll(): Promise<any> {
    this.logger.log('Get all products');

    return [];
  }
}

Dynamic Modules

  • This is a feature that allows you to easily create a custom module that can register and configure providers dynamically.

  • Following is an example of a dynamic module definition for a DatabaseModule:

core/database/database.providers.ts
import { Connection } from './connection.provider';

import { createConnection, Repository } from 'typeorm'; // Assuming TypeORM for database interaction

export function createDatabaseProviders(options, entities): any[] {
  const providers = entities.map((entity) => ({
    provide: `${entity}Repository`, // Dynamic provider name based on the entity
    useFactory: (connection: Connection) => connection.getRepository(entity),
    inject: [Connection],
  }));

  return [
    {
      provide: 'DATABASE_CONNECTION',
      useFactory: async () => {
        const connection = await createConnection({
          ...options,
          entities: entities,
        });
        return connection;
      },
    },
    ...providers, // Include additional providers related to entities
  ];
}
core/database/database.module.ts
import { Module, DynamicModule } from '@nestjs/common';

import { createDatabaseProviders } from './database.providers';
import { Connection } from './connection.provider';

@Module({
  providers: [Connection],
})
export class DatabaseModule {
  static forRoot(entities = [], options?): DynamicModule {
    const providers = createDatabaseProviders(options, entities);
    return {
      module: DatabaseModule,
      providers: providers,
      exports: providers,
    };
  }
}

Hint: The forRoot() method may return a dynamic module either synchronously or asynchronously.

  • This module defines the Connection provider by default (in the @Module() decorator metadata), but additionally - depending on the entities and options objects passed into the forRoot() method - exposes a collection of providers, for example, repositories.

  • The DatabaseModule can be imported and configured in the following manner:

import { Module } from '@nestjs/common';

import { DatabaseModule } from 'core/database/database.module';
import { User } from 'modules/user/user.entity.ts';
import { Product } from 'modules/product/product.entity.ts';

@Module({
  imports: [
    DatabaseModule.forRoot([User, Product], {
      type: 'postgres',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'password',
      database: 'mydatabase',
    }),
  ],
})
export class AppModule {}

Conclusion

  • Understanding and effectively utilizing modules in NestJS is essential for building scalable and maintainable applications.
  • Whether organizing feature-specific code, sharing providers, optimizing global functionality, or dynamically configuring modules, NestJS modules offer a robust foundation for application development.

References

Happy reading!