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:
- first: middlewares
- second: guards
- third: interceptors (interceptor is set to run before or after req handler)
- finally: the request handler
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:
- Services are one of the most common types of providers. They encapsulate the application's business logic and are designed to be injected into controllers, other services, or modules.
- Example:
UserService,LoggerService, etc.
providers: [UserService, LoggerService],
2. Repositories:
- Repositories are often used in the context of database interactions. They encapsulate the logic for querying the database and returning domain objects.
- Example:
UserRepository,ProductRepository, etc.
providers: [UserRepository, ProductRepository],
3. Factories:
- Factories are providers that create instances of classes or objects. They can be useful for complex object instantiation logic.
- Example:
ConnectionFactory,LoggerFactory, etc.
providers: [ConnectionFactory, LoggerFactory],
4. Helpers:
- Helpers are utility classes or functions that provide common functionality to other parts of the application.
- Example:
DateHelper,ValidationHelper, etc.
providers: [DateHelper, ValidationHelper],
5. Providers with Custom Tokens:
- You can also create providers with custom tokens. This is useful when you want to use interfaces or tokens to refer to specific implementations.
- Example:
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:
- You can include third-party services or integrations as providers to encapsulate their functionality.
- Example:
ThirdPartyApiService,EmailService, etc.
providers: [ThirdPartyApiService, EmailService],
7. Dynamic Providers:
- You can create providers dynamically at runtime using factories or async factories. This is useful when you need to configure providers based on runtime data.
- Example:
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:
-
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 {} -
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
importsarray. 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 {} - If the service you want to use is provided by another module, you can import that entire module into the current module's
-
Global Scope:
- If a provider needs to be accessed across multiple modules, you can make it a global provider by including it in the
providersarray of the root module (usuallyAppModule). Global providers are available throughout the application.
@Module({ providers: [SomeGlobalService], // ... }) export class AppModule {} - If a provider needs to be accessed across multiple modules, you can make it a global provider by including it in the
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:
-
Module A:
@Module({ providers: [XService], exports: [XService], }) export class ModuleA {} -
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:
importsis used to bring in functionality (providers, controllers, etc.) from other modules into the current module.exportsis 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.
-
Module A:
@Module({ providers: [ServiceA], exports: [ServiceA], // Makes ServiceA available for other modules importing ModuleA. }) 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) { // Both ServiceA and ServiceB are accessible here. } }
In this example:
ModuleAexportsServiceA.ModuleBimportsModuleA, makingServiceAavailable for injection in components, services, or controllers withinModuleB.ServiceBis specific toModuleBand does not need to be exported since it's not used in other modules.
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:
- Export services, factories, or other types of providers from the current module so that they can be injected into components or services of other modules.
@Module({
providers: [SomeService],
exports: [SomeService],
})
export class ModuleA {}
2. Controllers:
- Export controllers to allow them to be used in other modules. This is useful when you have controllers that need to handle routes across different modules.
@Module({
controllers: [SomeController],
exports: [SomeController],
})
export class ModuleB {}
3. Constants:
- Export constants or configuration values so that they can be accessed in other modules.
export const API_KEY = 'your_api_key';
@Module({
providers: [ConfigService],
exports: [ConfigService],
})
export class ConfigModule {}
4. Interfaces and Enums:
- Export interfaces and enums to ensure consistent data structures and values across different parts of the application.
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 DTO classes to represent data structures exchanged between different modules.
export class CreateUserDto {
readonly name: string;
readonly email: string;
}
@Module({
exports: [CreateUserDto],
})
export class UserModule {}
6. Custom Tokens:
- If you use custom tokens for dependency injection, you might need to export them so that other modules can inject providers using these 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 UsersService. UsersModule 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:
- Instantiating
UsersModule, including transitively importing other modules thatUsersModuleitself consumes, and transitively resolving any dependencies (see Custom providers). - Instantiating
AuthModule, and makingUsersModule's exported providers available to components inAuthModule(just as if they had been declared inAuthModule). - Injecting an instance of
UsersServiceinAuthService.
Dynamic modules
Modules Hierarchy
I have parent, child and grand child modules. where:
- parent Module, provides ServiceA and exports it
- child Module, imports parent Module, and provides ServiceB and exports it
- grand child, imports child Module, and provides ServiceC and exports it We have grand child imports child module, this means that it has also the service ServiceB ready to be injected. Is the service ServiceA belonging to the parent module ready implicitly to be injectable into the grand child module?
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:
-
Parent Module (Provides
ServiceA):@Module({ providers: [ServiceA], exports: [ServiceA], }) export class ParentModule {} -
Child Module (Imports Parent, Provides
ServiceB):import { ParentModule } from './parent.module'; @Module({ imports: [ParentModule], providers: [ServiceB], exports: [ServiceB], }) export class ChildModule {} -
Grandchild Module (Imports Child, Provides
ServiceC):import { ChildModule } from './child.module'; @Module({ imports: [ChildModule], providers: [ServiceC], exports: [ServiceC], }) export class GrandchildModule { constructor( private readonly serviceA: ServiceA, private readonly serviceB: ServiceB, private readonly serviceC: ServiceC, ) { // ServiceA, ServiceB, and ServiceC are accessible here. } }
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:
-
Purpose: Used for modules that should be configured globally in your application. For example, modules related to database connections, logging, authentication, etc.
-
Usage:
@Module({ imports: [ConfigModule.forRoot({ apiKey: 'your_api_key' })], providers: [AppService], }) export class AppModule {}
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:
-
Purpose: Used for feature modules or modules that are specific to certain parts of your application and need to be configurable for different parts of your application.
-
Usage:
@Module({ imports: [UsersModule.forFeature({ enableCache: true })], controllers: [UsersController], }) export class UsersModule {}
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.