Mastering NestJS: Unveiling Hidden Gems in Controllers, Providers, and Modules

Mastering NestJS: Unveiling Hidden Gems in Controllers, Providers, and Modules

NestJS is renowned for its robustness and scalability, making it the go-to framework for building server-side applications with Node.js. If you're a Javascript backend developer or a full-stack looking to level up your NestJS skills, you're in the right place. In this blog post, I'll talk about some lesser-known features of NestJS related to Controllers, Providers, and Modules. By mastering these features, you can take your NestJS projects to the next level and open up new possibilities, making your code more efficient and maintainable.

1. The Magic of Dynamic Modules

In NestJS, modules are the core of structuring your application. While you're probably accustomed to creating static modules, dynamic modules are a hidden treasure. Dynamic modules allow you to create modules programmatically, enabling more flexibility in your application's architecture.

Here's a simple example of a dynamic module:

import { Module, DynamicModule } from '@nestjs/common';
import { ConfigService } from './config.service';

@Module({})
export class DynamicModule {
  static forRoot(options: string): DynamicModule {
    const providers = createProviders(options);
    return {
      module: DynamicModule,
      providers,
      exports: providers,
    };
  }
}

In this example, we create a DynamicModule that can be customized using the forRoot method. This dynamic approach is particularly useful when you need to configure modules based on runtime conditions.

2. Custom Decorators for Controllers

Controllers are at the heart of any NestJS application, responsible for handling incoming requests. While you're likely familiar with the built-in decorators like @Get() and @Post(), you can create your custom decorators to encapsulate common functionality.

Here's how you can create a custom decorator for request validation:

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const CustomHeader = createParamDecorator(
  (customHeaderName: string, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.headers[customHeaderName];
  },
);

This CustomHeader decorator can easily be applied to parameters in your route handler methods and allow you to access and modify the values of those parameters before they are passed to the method:

@Controller('example')
export class ExampleController {
  @Get('custom-header')
  getCustomHeaderValue(@CustomHeader('X-Custom-Header') customHeaderValue: string) {
    return `Custom Header Value: ${customHeaderValue}`;
  }
}

When you make a request to the /example/custom-header endpoint with the "X-Custom-Header" header, the value of the header will be extracted and used in the route handler. This allows you to create custom decorators to simplify handling specific parts of the request in your NestJS application.

3. Provider Scopes

Providers in NestJS are responsible for creating and managing instances. By default, providers have a singleton scope, meaning there's only one instance of a provider throughout the application. However, you can leverage different provider scopes for more complex scenarios.

  • Request Scope: Use REQUEST scope to create a new instance of a provider for every incoming HTTP request. This is particularly useful when you need request-specific data.

  • Transient Scope: The TRANSIENT scope creates a new instance of a provider every time it's requested. This can be handy when you want to maintain state across different parts of your application.

  • Custom Scopes: You can even define custom scopes based on your application's requirements.

Here's an example of using the request scope:

import { Module, Scope } from '@nestjs/common';
import { DatabaseService } from './database.service';

@Module({
  providers: [
    {
      provide: DatabaseService,
      useClass: DatabaseService,
      scope: Scope.REQUEST,
    },
  ],
})
export class AppModule {}

By defining the scope: Scope.REQUEST, a new DatabaseService instance will be created for each HTTP request.

4. Asynchronous Configuration

Configuring your NestJS application is crucial, but some configurations may require fetching data asynchronously. In such cases, the ConfigModule can be enhanced to support asynchronous configuration.

Here's how to create an asynchronous configuration provider:

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AsyncConfigService } from './async-config.service';

@Module({
  imports: [ConfigModule.forRoot({ isGlobal: true })],
  providers: [AsyncConfigService],
  exports: [AsyncConfigService],
})
export class AsyncConfigModule {}

In your AsyncConfigService, you can fetch configuration values asynchronously:

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class AsyncConfigService {
  constructor(private configService: ConfigService) {}

  async getAsyncConfigValue(key: string): Promise<string> {
    return await this.configService.get(key);
  }
}

Now, you can inject and use AsyncConfigService throughout your application to access asynchronous configuration data.

That's it for today. Happy Coding!