Modules: Organizing Your Application
Understanding NestJS modules, creating custom modules, and importing modules into each other.
Building Custom NestJS Modules
Creating Custom Modules: An Overview
In NestJS, modules are the fundamental building blocks for structuring your application. They act as containers for related components like controllers, providers (services, repositories), and other modules. Creating custom modules allows you to encapsulate and organize application logic, promoting reusability, maintainability, and testability. Modules facilitate dependency injection and manage the lifecycle of their components.
By creating modular applications, you're able to separate concerns and simplify your codebase, making it easier to understand, debug, and scale. Custom modules are essential for any non-trivial NestJS application.
A Detailed Guide to Building Custom NestJS Modules
1. Generating a Module
The easiest way to create a module is using the Nest CLI:
nest generate module my-module
This command creates a directory named my-module
(or the name you provide) and generates two files: my-module.module.ts
and (potentially) my-module.module.spec.ts
(for unit tests). The module file will contain a basic module definition.
2. Understanding the Module Structure
Open the my-module.module.ts
file. It should look something like this:
import { Module } from '@nestjs/common';
@Module({
imports: [],
controllers: [],
providers: [],
exports: [],
})
export class MyModule {}
Let's break down each part:
@Module()
: This is the module decorator, which tells NestJS that this class is a module.imports
: This array is used to import other modules into this module. This allows components within this module to access the exported components of other modules.controllers
: This array lists the controllers defined within this module. Controllers handle incoming requests and delegate logic to providers.providers
: This array lists the providers (services, repositories, factories, etc.) that are instantiated by the NestJS injector and can be injected into controllers or other providers within this module.exports
: This array lists the providers that this module makes available to other modules that import it. Only providers listed here can be used by other modules.
3. Defining Providers
Providers are classes responsible for handling business logic, data access, or any other functionality within your application. Let's create a simple provider:
- Create a file named
my-service.service.ts
inside themy-module
directory. - Add the following code to the file:
import { Injectable } from '@nestjs/common';
@Injectable()
export class MyService {
getData(): string {
return 'Hello from MyService!';
}
}
The @Injectable()
decorator marks the class as a provider that can be injected by NestJS.
- Register the provider in the
my-module.module.ts
file:
import { Module } from '@nestjs/common';
import { MyService } from './my-service.service';
@Module({
imports: [],
controllers: [],
providers: [MyService],
exports: [MyService], // Make MyService available to other modules
})
export class MyModule {}
4. Defining Controllers
Controllers handle incoming requests and delegate the logic to providers. Let's create a simple controller:
- Create a file named
my-controller.controller.ts
inside themy-module
directory. - Add the following code to the file:
import { Controller, Get } from '@nestjs/common';
import { MyService } from './my-service.service';
@Controller('my-controller') // Define the route prefix
export class MyController {
constructor(private readonly myService: MyService) {}
@Get()
getData(): string {
return this.myService.getData();
}
}
The @Controller()
decorator defines the route prefix for the controller. The constructor injects the MyService
provider. The @Get()
decorator defines a route that handles GET requests to the root path ('/my-controller').
- Register the controller in the
my-module.module.ts
file:
import { Module } from '@nestjs/common';
import { MyService } from './my-service.service';
import { MyController } from './my-controller.controller';
@Module({
imports: [],
controllers: [MyController],
providers: [MyService],
exports: [MyService],
})
export class MyModule {}
5. Using the Module in Your Application
To use your custom module, you need to import it into your application's root module (app.module.ts
):
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MyModule } from './my-module/my-module.module';
@Module({
imports: [MyModule], // Import your custom module
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Now, you can access the endpoints defined in your MyController
by navigating to /my-controller
in your browser.
6. Sharing Providers with Other Modules
If you want to make a provider from your custom module available to other modules, you need to export it in the exports
array of your module definition. As shown in the previous examples, we've exported `MyService` from `MyModule`. Any module that imports `MyModule` will now be able to inject `MyService`.
7. Module Configuration and Options
Sometimes, you need to configure your module with options. For example, you might want to pass in database connection details or API keys. There are several ways to handle module configuration in NestJS. One common approach is to use a configuration service:
- Create a configuration service (e.g., `my-module-config.service.ts`) within your module.
- Inject the configuration service into your providers or controllers.
- Use the `forRoot` static method pattern to configure the module from the root module.
Example of configuring using a static `forRoot` method
// my-module.module.ts
import { Module, DynamicModule } from '@nestjs/common';
import { MyService } from './my-service.service';
import { MyController } from './my-controller.controller';
import { MyModuleOptions } from './interfaces/my-module-options.interface';
import { MyModuleConfigService } from './my-module-config.service';
@Module({})
export class MyModule {
static forRoot(options: MyModuleOptions): DynamicModule {
return {
module: MyModule,
providers: [
{
provide: 'MODULE_OPTIONS', // String token, best practice to not use string literals, use a symbol
useValue: options,
},
MyService,
MyModuleConfigService,
],
controllers: [MyController],
exports: [MyService],
};
}
}
// interfaces/my-module-options.interface.ts
export interface MyModuleOptions {
apiKey: string;
// other options
}
// my-module-config.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { MyModuleOptions } from './interfaces/my-module-options.interface';
@Injectable()
export class MyModuleConfigService {
constructor(@Inject('MODULE_OPTIONS') private readonly options: MyModuleOptions) {}
getApiKey(): string {
return this.options.apiKey;
}
}
// In app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MyModule } from './my-module/my-module.module';
@Module({
imports: [MyModule.forRoot({ apiKey: 'YOUR_API_KEY' })],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
8. Asynchronous Module Configuration
For asynchronous module configuration (e.g., reading config from a database or external source), use the forRootAsync
static method. This allows you to use factories and dependency injection to configure your module asynchronously.
// my-module.module.ts
import { Module, DynamicModule, Provider } from '@nestjs/common';
import { MyService } from './my-service.service';
import { MyController } from './my-controller.controller';
import { MyModuleOptions } from './interfaces/my-module-options.interface';
import { MyModuleConfigService } from './my-module-config.service';
import { MyModuleAsyncOptions } from './interfaces/my-module-async-options.interface';
import { ConfigService } from '@nestjs/config'; // Assuming you are using @nestjs/config
@Module({})
export class MyModule {
static forRootAsync(options: MyModuleAsyncOptions): DynamicModule {
return {
module: MyModule,
imports: options.imports || [], // Import necessary modules (e.g., ConfigModule)
providers: [
this.createAsyncProviders(options), // Call a helper function
MyService,
MyModuleConfigService,
],
controllers: [MyController],
exports: [MyService],
};
}
private static createAsyncProviders(options: MyModuleAsyncOptions): Provider {
return {
provide: 'MODULE_OPTIONS',
useFactory: async (...args: any[]) => {
const moduleOptions = await options.useFactory(...args);
return moduleOptions;
},
inject: options.inject || [], // Inject dependencies for the factory
};
}
}
// interfaces/my-module-async-options.interface.ts
import { ModuleMetadata, Type } from '@nestjs/common';
import { MyModuleOptions } from './my-module-options.interface';
export interface MyModuleAsyncOptions extends Pick {
useFactory: (...args: any[]) => Promise | MyModuleOptions;
inject?: any[];
}
// In app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MyModule } from './my-module/my-module.module';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot(), // Ensure ConfigModule is loaded
MyModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
apiKey: configService.get('MY_API_KEY'), // Read from environment variable
}),
inject: [ConfigService],
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
The forRootAsync
method enables asynchronous configuration using a factory function. It's crucial for configurations dependent on external sources like environment variables or databases. This example uses @nestjs/config
to illustrate reading an API key from an environment variable.
9. Global Modules
Sometimes, you want to make a module's providers available throughout your entire application without having to import the module into every other module. You can achieve this by making your module a "global" module. To do this, add the @Global()
decorator above the @Module()
decorator. Be careful when using global modules, as they can make dependency management more difficult if overused. Use them sparingly for truly global providers like configuration services or logging services.
import { Module, Global } from '@nestjs/common';
import { MyService } from './my-service.service';
@Global()
@Module({
providers: [MyService],
exports: [MyService],
})
export class MyModule {}
Best Practices
- **Keep Modules Focused:** Each module should have a clear and specific purpose.
- **Use Dependency Injection:** Leverage NestJS's dependency injection system to manage dependencies between modules.
- **Export Providers Wisely:** Only export the providers that need to be accessible from other modules.
- **Use `forRoot` or `forRootAsync` for Configuration:** Follow the static method pattern for configuring modules with options.
- **Consider Global Modules Carefully:** Use global modules only for truly global providers.
- **Test Your Modules:** Write unit tests for your modules to ensure they function correctly.