NestJS Notes

2022-01-25

NestJS Notes

Created: January 25, 2022 10:37 AM

Dependency Injection

in Nestjs a service doesn’t call directly it’s dependencies, in other words he is not allowed to create his own dependencies like that

// DON'T DO THIS IN REAL APPs
import { MessagesRepository } from './messages.repository';
 
export class MessagesService {
	messagesRepo: MessagesRepository;
 
	constructor(){
		this.messagesRepo = new MessagesRepository();
	}
} 

Instead of that we use dependency injection

Request Journey

Request pass by order via:

The Nest IoC container:

The IoC container is an internal mechanism provided by NestJS that manages the instantiation and resolution of these components based on their dependencies and the configuration provided by modules like AppModule. The IoC container is responsible for handling the lifecycle of objects, managing dependency injection, and ensuring the proper instantiation of components throughout the application.

Module

A module configuration utilizes the IoC container under the hood to manage the dependencies and instantiate the required components, the module itself is not the IoC container. Instead, it's a structured way of defining how components are related and should be instantiated within the application context.

import { Module } from '@nestjs/common';
import { SomeController } from './some/some.controller';
import { SomeService } from './some/some.service';
 
@Module({
  controllers: [SomeController],
  providers: [SomeService],
})
export class AppModule {}
 

Imports

the property importswithin a module serves only to import other modules. right?

Yes, that's correct. In the context of a NestJS module, the imports property is specifically used to import other modules. It does not directly import other elements like services, classes, or constants into the module. The imports property is used to bring in functionality from other modules and make their components (such as services, controllers, etc.) available within the current module. This helps organize and modularize your application by breaking it down into smaller, manageable pieces.

imports: [OtherModule, AnotherModule],

Providers

The providers property is where you define the providers that are scoped to the current module. These providers can be services, repositories, factories, etc., that are used within the current module. Providers listed here are accessible only within the current module.

providers: [AppService, DatabaseService],

the above syntax of standard providers is a short-hand of the following syntax:

providers: [
  {
    provide: AppService,
    useClass: AppService,
  },
	{
    provide: DatabaseService,
    useClass: DatabaseService,
  },
];

Additional details

In the context of a NestJS module, the providers array is used to define the set of providers (such as services, repositories, factories, etc.) that are scoped to the current module. Providers are objects that NestJS can create and inject into other components (controllers, other services, etc.) which makes them a fundamental building block for creating modular and maintainable applications.

Here's what you can provide in the providers field within a module:

1. Services:

providers: [UserService, LoggerService],
 

2. Repositories:

providers: [UserRepository, ProductRepository],
 

3. Factories:

providers: [ConnectionFactory, LoggerFactory],
 

4. Helpers:

providers: [DateHelper, ValidationHelper],
 

5. Providers with Custom Tokens:

const CustomLogger = {
  provide: 'CUSTOM_LOGGER',
  useValue: new Logger(),
};
 
@Module({
  providers: [CustomLogger],
})
export class AppModule {}
 

In the above example, CUSTOM_LOGGER is a custom token used to identify the provider.

6. Third-Party Services:

providers: [ThirdPartyApiService, EmailService],
 

7. Dynamic Providers:

const connectionFactory = {
  provide: 'CONNECTION',
  useFactory: async (optionsService: OptionsService) => {
    const options = await optionsService.getDatabaseOptions();
    return createConnection(options);
  },
  inject: [OptionsService],
};
 
@Module({
  providers: [connectionFactory],
})
export class AppModule {}
 

In this dynamic provider example, the connectionFactory provider is created based on the result of optionsService.getDatabaseOptions().

Providers defined within a module are available for injection into other components of the same module, or they can be exported and used in other modules if listed in the exports array of the module. This modularity and encapsulation make the application more maintainable and testable.

this mean that services belonging to other modules cannot be passed to the providers array. right?

Yes, you're absolutely correct. In the providers array within a NestJS module, you typically include services, factories, repositories, helpers, and third-party services that are scoped to the current module. These providers are available for injection into other components (such as controllers or other services) within the same module.

Providers from other modules cannot be directly included in the providers array of a different module. However, if you need to use a service from another module within your current module, you have a few options:

  1. Importing Services from Other Modules:

    • You can import services from other modules into the current module where you need to use them. Then, you can inject these services into your components.
    import { SomeService } from '../other-module/some.service';
     
    @Module({
      imports: [OtherModule],
      providers: [SomeService],
    })
    export class CurrentModule {}
     
  2. Module Imports:

    • If the service you want to use is provided by another module, you can import that entire module into the current module's imports array. This imports all the providers from the imported module.
    import { OtherModule } from '../other-module/other.module';
     
    @Module({
      imports: [OtherModule],
      providers: [/* providers from OtherModule are available here */],
    })
    export class CurrentModule {}
     
  3. Global Scope:

    • If a provider needs to be accessed across multiple modules, you can make it a global provider by including it in the providers array of the root module (usually AppModule). Global providers are available throughout the application.
    @Module({
      providers: [SomeGlobalService],
      // ...
    })
    export class AppModule {}
     

By managing providers in this way, you can ensure that services are appropriately encapsulated within their respective modules while still being accessible to other parts of the application where needed. This approach promotes modularity, separation of concerns, and maintainability in your NestJS application.

Note:

When you want to use a service (X) from another module (A) in your current module (B), adding A to the imports array of module B is generally enough. You do not need to include service X in the providers array of module B if it's already provided in module A.

Here's the typical scenario:

  1. Module A:

    @Module({
      providers: [XService],
      exports: [XService],
    })
    export class ModuleA {}
     
  2. Module B:

    import { ModuleA } from 'path/to/module-a';
     
    @Module({
      imports: [ModuleA],
      providers: [/* You don't need to include XService here */],
    })
    export class ModuleB {}
     

In this example, XService is provided in ModuleA. By importing ModuleA into ModuleB, you make XService accessible to components within ModuleB without needing to explicitly list it in ModuleB's providers array.

However, there might be cases where you want to override or customize the behavior of XService specifically for ModuleB. In such situations, you can include XService in ModuleB's providers array. This would effectively shadow the service provided by ModuleA. Still, it's important to note that this approach should be used cautiously, as it might lead to unexpected behavior and can make the code harder to maintain due to the implicit overrides.

In general, relying on the imports array to bring in the required services from other modules promotes modularity and ensures that the dependencies between modules are clear and explicit. Overriding services should be done deliberately and with a good understanding of the implications it might have on the application's behavior.

Exports

until now, I don't quit get the utility of exports, since imports makes everything within an external module available within the current module.

You're right; the concept of exports in NestJS might seem a bit confusing, but it plays a crucial role in managing the visibility of providers between modules. Let's clarify the utility of exports and how it impacts module organization.

In NestJS:

  1. imports is used to bring in functionality (providers, controllers, etc.) from other modules into the current module.
  2. exports is used to make your module's providers (or other components) available for use in other modules that import the current module.

Here's a scenario where exports becomes important:

Example Scenario:

Consider ModuleA providing ServiceA and ModuleB that wants to use ServiceA.

  1. Module A:

    @Module({
      providers: [ServiceA],
      exports: [ServiceA], // Makes ServiceA available for other modules importing ModuleA.
    })
    export class ModuleA {}
     
  2. Module B:

    import { ModuleA } from 'path/to/module-a';
     
    @Module({
      imports: [ModuleA], // Import ModuleA to use ServiceA.
      providers: [ServiceB], // ServiceB is defined within ModuleB.
    })
    export class ModuleB {
      constructor(private readonly serviceA: ServiceA, private readonly serviceB: ServiceB) {
        // Both ServiceA and ServiceB are accessible here.
      }
    }
     

In this example:

By exporting ServiceA from ModuleA, you're explicitly allowing other modules (such as ModuleB) to access and use ServiceA. This export mechanism helps maintain control over which providers are meant to be used outside of the current module. It enhances modularity and encapsulation in your NestJS application by clearly defining the public interface of your modules.

In summary, exports is essential for controlling the visibility of providers, ensuring that only the necessary services, components, or modules are accessible outside the module boundaries. It helps manage the complexity of larger applications and promotes a clean, organized code structure.

Confirm one more thing. If we take the module A, that has 2 services ServiceA and ServiceAA and exports only ServiceA. like this:

**@Module({
  providers: [ServiceA, ServiceAA],
  exports: [ServiceA], // Makes ServiceA available for other modules importing ModuleA.
})
export class ModuleA {}**

inside moduleB, will the service named ServiceAA be available or no? Knowing that ModuleA is imported within ModuleB like in the example you gave in your last answer.

In the scenario you described, where ModuleA exports ServiceA but does not export ServiceAA, ServiceAA will not be directly accessible in ModuleB.

Here's a summary of the scenario:

// Module A
@Module({
  providers: [ServiceA, ServiceAA],
  exports: [ServiceA],
})
export class ModuleA {}
 
// Module B
import { ModuleA } from 'path/to/module-a';
 
@Module({
  imports: [ModuleA], // Import ModuleA to use ServiceA.
  providers: [ServiceB], // ServiceB is defined within ModuleB.
})
export class ModuleB {
  constructor(private readonly serviceA: ServiceA, private readonly serviceB: ServiceB) {
    // ServiceA is accessible here, but ServiceAA is not.
  }
}
 

In ModuleB, only ServiceA is accessible because it's exported by ModuleA. ServiceAA is not accessible directly in ModuleB because it was not included in the exports array of ModuleA.

Remember, the exports array in ModuleA specifies which components/services are accessible in other modules. In this case, only ServiceA is accessible in ModuleB because it's included in the exports array of ModuleA. ServiceAA is private to ModuleA and cannot be directly injected into components, controllers, or services within ModuleB.

now I understand the utility of exports, can you tell me what do we export exactly in addition of services?

In the exports array within a NestJS module, you can export various elements to make them accessible in other modules that import the current module. Here's what you can export in the exports array:

1. Providers:

@Module({
  providers: [SomeService],
  exports: [SomeService],
})
export class ModuleA {}
 

2. Controllers:

@Module({
  controllers: [SomeController],
  exports: [SomeController],
})
export class ModuleB {}
 

3. Constants:

export const API_KEY = 'your_api_key';
 
@Module({
  providers: [ConfigService],
  exports: [ConfigService],
})
export class ConfigModule {}
 

4. Interfaces and Enums:

export interface User {
  id: number;
  name: string;
}
 
export enum UserRole {
  ADMIN = 'admin',
  USER = 'user',
}
 
@Module({
  exports: [User, UserRole],
})
export class SharedModule {}
 

5. DTOs (Data Transfer Objects):

export class CreateUserDto {
  readonly name: string;
  readonly email: string;
}
 
@Module({
  exports: [CreateUserDto],
})
export class UserModule {}
 

6. Custom Tokens:

export const CUSTOM_LOGGER = 'CustomLoggerToken';
 
@Module({
  providers: [{ provide: CUSTOM_LOGGER, useClass: CustomLogger }],
  exports: [CUSTOM_LOGGER],
})
export class LoggerModule {}
 

By exporting these elements in the exports array of a module, you make them available for import in other modules. This enables better code organization, reusability, and separation of concerns in a NestJS application. Other modules can import the exported elements without having to know the internal details of the exporting module, promoting a modular and maintainable codebase.

Static Module Binding

(regular modules combination)

providers defined in a module are visible to other members of the module without the need to export them. When a provider needs to be visible outside of a module, it is first exported from its host module, and then imported into its consuming module.

Let's walk through a familiar example.

First, we'll define a UsersModule to provide and export a UsersServiceUsersModule is the host module for UsersService.

import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
 
@Module({
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

Next, we'll define an AuthModule, which imports UsersModule, making UsersModule's exported providers available inside AuthModule:

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
 
@Module({
  imports: [UsersModule],
  providers: [AuthService],
  exports: [AuthService],
})
export class AuthModule {}

These constructs allow us to inject UsersService in, for example, the AuthService that is hosted in AuthModule:

import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
 
@Injectable()
export class AuthService {
  constructor(private usersService: UsersService) {}
  /*
    Implementation that makes use of this.usersService
  */
}

We'll refer to this as static module binding. All the information Nest needs to wire together the modules has already been declared in the host and consuming modules. Let's unpack what's happening during this process. Nest makes UsersService available inside AuthModule by:

  1. Instantiating UsersModule, including transitively importing other modules that UsersModule itself consumes, and transitively resolving any dependencies (see Custom providers).
  2. Instantiating AuthModule, and making UsersModule's exported providers available to components in AuthModule (just as if they had been declared in AuthModule).
  3. Injecting an instance of UsersService in AuthService.

Dynamic modules

Modules Hierarchy

I have parent, child and grand child modules. where:

Yes, based on your module hierarchy, if the grandchild module imports the child module, which in turn imports the parent module, the services provided by the parent module (ServiceA in this case) will be implicitly available for injection into components, services, or controllers within the grandchild module.

Here's how the hierarchy looks:

In the GrandchildModule, both ServiceA and ServiceB are implicitly injectable because GrandchildModule imports ChildModule, and ChildModule imports ParentModule. Therefore, ServiceA from the parent module and ServiceB from the child module are available for injection into components or services within the grandchild module.

This hierarchical structure enables you to manage the visibility and accessibility of services across modules in a structured and organized manner, promoting modularity and maintainability in your NestJS application.

ForRoot & ForFeature

In NestJS, the forRoot and forFeature functions are commonly used when creating modular, configurable, and reusable modules, especially for feature modules, database modules, or modules that require configuration options. These functions enable you to pass configuration parameters when importing a module, allowing you to customize the behavior of the imported module based on the specific requirements of your application or a specific feature.

Here's how they are typically used:

forRoot Function:

In this example, forRoot is used to configure the ConfigModule globally with a specific API key. This configuration will be used across the entire application.

forFeature Function:

In this example, forFeature is used to configure the UsersModule with specific options (enableCache: true) that are specific to the UsersModule. These options might be different from the configuration used in other parts of the application.

By using these functions, you can create modules that can be easily configured and reused in different parts of your application. They provide a flexible way to customize module behavior without creating multiple copies of similar modules with slight variations in functionality.

It's worth noting that the forRoot and forFeature patterns are not specific to NestJS but are a common pattern in many modern web frameworks. They offer a convenient and organized way to handle module configuration and dependency injection.