How To Use Configurable Module Builders in Nest.js v9

Dynamic Nest.js modules for customized backend services

Agustinus Theodorus
Geek Culture
Published in
9 min readNov 7, 2022

--

Photo by Hannah Troupe on Unsplash

Nest.js is a web framework made exclusively in TypeScript. Most developers exposed to Nest.js will often recognize something deeply familiar, it’s a strong object-oriented programming model, and its syntax is very similar to another framework, Angular.

Nest.js code often will force you to create an optimal design pattern. However, developers moving to Node.js and TypeScript from a Java or ASP.NET background will quickly appreciate how Nest.js is structured. Concepts like dependency injection, encapsulation, classes, and class annotations (decorators) are all available in Nest.js.

In Nest.js creating a custom module to reuse is quite simple and is, above all, encouraged. Having a module encapsulated into small reusable chunks will help increase the development speed of a Nest.js application and will give you bonus points if you decide to release those modules in the wild as open source packages.

What Are Nest.js Modules?

But before continuing, what are Nest.js modules?

Modules are encapsulated sets of code to be injected into a Nest.js application. You can use modules to create custom services meant to do specific tasks. For example, TypeORM is a TypeScript-based ORM. The Nest.js team created a module that will inject an open database connection allowing for database commands and queries from the injected module.

Nest.js modules are the backbone of the framework’s robust dependency injection mechanism. Dependency injection is an application development pattern aiming to decouple the dependencies between two classes (or modules). Instead of having strictly defined dependencies for each class, you can use an interface to have a somewhat “contract” of how your dependency should behave while having no literal definition of how it should run.

A decoupled architecture allows for versatile applications and creates a plug-and-play behavior for each module in the application.

Nest.js Module State Management

Nest.js modules are singletons by default. You only need to initiate a module once. While creating singletons for every module can seem excessive from an engineering point of view, Nest.js will initialize singletons on a component level.

Module scopes in Nest.js

There are three injection scopes of modules in Nest.js:

  1. Request-level modules
  2. Component-level modules (transient)
  3. Shared application-level modules

Most Nest.js modules are application-level modules by default, or you can also call them globally shared modules. But not every module can be a global module. For example, some of them need to remain transient/request level.

For example, if you need an application-level read-only module, your best bet is to use global shared modules. Data stored within the module will not often change, so it can be deferred as an application-level singleton to conserve memory and create a globally accessible class. Modules that have the @Global decorator removes redundancy in both codes and component levels since you don’t need to reinitialize the module.

To better understand state preservation on a modular level, if you have a constant within either a module with a transient or request scope, it will be an immutable variable until the module destroys it on garbage collection. However, when using a global module spanning the entire application, it will be destroyed only at the end of an application’s lifetime.

Preventing data racing conditions when using singletons

Another thing to be cautious about when using Singletons is the data racing problem. Node.js is not immune to data racing conditions, and so is Nest.js. Data race conditions are when two separate processes try to update the same block of data simultaneously. Because the objects are accessible globally, simultaneous data execution may result in lost data points on execution. The best practice to avoid data race conditions is to create a global read-only module and be more deliberate on each module’s injection scopes.

Global modules are the most susceptible to data race conditions, and using global modules to communicate or manage states between components will result in an anti-pattern.

But why can’t the same be said of transient component-level modules?

At the component level, the encapsulation barriers only extend to the component’s needs. Each transient module provider will have a dedicated instance. The separation of concerns at a component level is usually more granular, making it more predictable than in large-scale applications.

And the same can be said for the request-level singletons, albeit on a smaller scale.

Short Summary

In summary, there are three injection scopes of modules in Nest.js:

  1. Request-level modules
  2. Component-level modules (transient)
  3. Shared application-level modules (global)

Each has its benefits and disadvantages, with data race being the most common problem for global modules. Most global modules should be read-only, and Nest.js will only set the original state once during initialization.

Component-level modules have more nuances; more specifically, you can use them for state management on a smaller scale because of their predictability. The granular encapsulation singletons provide on a component level makes it a perfect choice for component-level state management.

Note: the data race condition is only limited to the state of each independent module. Modifying data in external applications like databases should not be a problem since databases have their own data race solution.

Configurable Module Builders in Nest.js

Default Nest.js modules are static and unconfigurable. On the other hand, configurable model builders are dynamic module factories that can churn out different modules based on the variables passed on initialization.

Dynamic modules in Nest.js

But before you start doing a configurable module, you need to understand the basics of a dynamic module. Their use cases usually revolve around creating a non-static module that can receive parameters from external APIs to change how the module behaves, specifically, how each module processes data.

For example, you create a module for querying data from databases, but you don’t want to hardcode for specific database providers. So how do you solve the problem?

First, you need to create a module that has a configuration function. The configuration function will have a database provider interface as a parameter. The provider interface has all the essential functions an application needs to connect and query a database. Because you use an interface as a parameter, you can inject different database providers as long as the provider extends the interface.

The underlying business logic will still be the same, but the database providers will change according to the one you supplied on initialization. Thus, your module will no longer be static and will be dynamic.

That’s essentially why all configurable modules are dynamic modules under the hood.

Architecting a basic configurable Nest.js module

As an example, you will be creating a custom Nest.js module that reads data from the .env file using the process.env API in the dotenv package. The module will function as a configurable proxy that you can use within your projects.

Module architecture, image by author

The architecture of the proxy module will seem redundant because you can access the process.env variable directly without dependency injection. But for the sake of simplicity, you will be using this architecture to grasp how Nest.js modules work thoroughly.

Your proxy module will retrieve the process.env on initialization and will store it in it’s env property. A Nest.js module is a singleton by default, so you only need to initialize it once. You can execute the getEnv function to retrieve your env variables, it will function as a getter to the dynamic env property.

You can add a function on initialization to accept parameters and create a dynamic module, making it configurable. In this case, the withConfig function will be the configurable init function.

How to create a basic configurable Nest.js module

Install the @nest/cli globally.

npm i -g @nest/cli

Then, generate a new Nest.js app.

nest new configurable-module-builder-examples

Choose any package manager you prefer, but this tutorial mainly uses Yarn. You can see the code you generated so far by going to the step-1 branch here.

A new Nest.js project has all the modules on one level; you need to refactor it before you can continue. Copy the module, controller, and service into a folder named api-modules and rename all the file and variable names from App to Api.

Create a new AppModule file and inject the ApiModule in the imports.

import { Module } from '@nestjs/common';
import { ApiModule } from './api-module/api.module';
@Module({
imports: [ApiModule],
})
export class AppModule {}

If you fail to follow along, check the step-2 branch in the repository here.

Next, you can start creating the process.env proxy module. You need the dotenv package to access .env files, so install the dependency by running the following:

yarn add dotenv

Create a new folder src/env-proxy-module and create two files env-proxy.module.ts:

import { Global, Module } from '@nestjs/common';
import { EnvProxyService } from './env-proxy.service';
@Global()
@Module({
providers: [EnvProxyService],
exports: [EnvProxyService],
})
export class EnvProxyModule {}

Notice the @Global decorator is used to automatically inject the module’s exports to any child of the injected component. You don’t have to repeatedly import the EnvProxyModule on every module. You only need to add it as an import in the main AppModule.

import { EnvProxyModule } from './env-proxy-module/env-proxy.module';@Module({
imports: [ApiModule, EnvProxyModule],
})
export class AppModule {}

Then, create the service file, env-proxy.service.ts:

import { Injectable } from '@nestjs/common';require('dotenv').config(); // eslint-disable-line@Injectable()
export class EnvProxyService {
public readonly env: NodeJS.ProcessEnv;
constructor() {
this.env = process.env;
}
}

To test the EnvProxyModule you can create an .env file, with a DATA parameter inside:

DATA=Hello World

Try the module out by returning the environment variable on a GET request:

import { EnvProxyService } from '../env-proxy-module/env-proxy.service';@Injectable()
export class ApiService {
constructor(private readonly envProxy: EnvProxyService) {}
getHello(): string {
return this.envProxy.env.DATA;
}
}

Run the development server:

yarn start:dev

Open your browser and go to localhost:3000. You should get a Hello World text in return. The complete code for this step can be seen in the step-3 branch here.

You have created a simple module, but it’s not configurable yet. You can use the ConfigurationModuleBuilder to do it. The ConfigurableModuleBuilder is a new feature provided in Nest.js v9, and its purpose is to reduce the amount of boilerplate code you need to write to create a configurable dynamic module.

Inside src/env-proxy-module create a new file env-proxy.definition.ts:

import { ConfigurableModuleBuilder } from '@nestjs/common';export interface EnvProxyModuleOptions {
exclude: string[];
}
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
new ConfigurableModuleBuilder<EnvProxyModuleOptions>({
moduleName: 'EnvProxy',
})
.build();

The EnvProxyModuleOptions interface represents the object you pass through the config file on initialization. In this case, you will give an array of the environment variables you want to exclude from the module.

Make the EnvProxyModule extend the ConfigurableModuleBuilder:

@Global()
@Module({
providers: [EnvProxyService],
exports: [EnvProxyService],
})
export class EnvProxyModule extends ConfigurableModuleClass {}

Continue to the EnvProxyService class and implement the following:

@Injectable()
export class EnvProxyService {
public readonly env: NodeJS.ProcessEnv;
constructor(@Inject(MODULE_OPTIONS_TOKEN) private options: EnvProxyModuleOptions) {
this.env = process.env;
options.exclude.forEach(val => {
delete this.env[val];
});
}
}

You can retrieve the EnvProxyModuleOptions object from the constructor and the @Inject decorator. You will add the main business logic for excluding environment variables to the EnvProxyService constructor. To test it out, you can create a second environment variable DATA2.

DATA=Hello World
DATA2=Hello World2

On the AppModule imports, add a register function and insert a EnvProxyModuleOptions.

imports: [ApiModule, EnvProxyModule.register({
exclude: [
"DATA"
]
})],

The module will exclude the DATA variable to check whether or not the exclusion is working, edit the ApiService:

getHello(): string {
return this.envProxy.env.DATA ?? this.envProxy.env.DATA2;
}

When you visit localhost:3000 you will get Hello World2.

Congratulations, you have created your first configurable module using the configurable module builder! You can check the final code base here.

If your configuration is more sophisticated than the previous example, you can rely on the factory pattern instead:

Using a useFactory allow you to call asynchronous code to help you configure your builder. For the final asynchronous example, you can find it here.

Summary

NestJS is an object-oriented web framework built in TypeScript. NestJS uses strong object-oriented principles under the hood and offers many functions, including dependency injection, classes, class annotations via decorators, and strong encapsulation.

State management in NestJS varies on the modular level. Most modules’ encapsulation is limited to the component level, while few have global shared states on the application level. All NestJS modules use singletons under the hood. If not appropriately made, having shared modules that you can use globally can cause data race conditions.

Most shared modules should be read-only, but there are some cases where shared modules are practical, logging and connecting to a message queue. Otherwise, you’ll only need modules on the component level.

Dynamic modules are non-static configurable modules. Essentially, all configurable modules are dynamic. A configurable module uses the factory pattern to create different modules according to the parameters given during initialization.

Writing custom NestJS modules is quite straightforward. You can access the code samples on GitHub and use the different branches to navigate each stage of the project.

I hope you enjoyed this article. Leave a comment if you have any questions.

Happy coding!

Originally published at https://blog.logrocket.com on September 22, 2022.

--

--

Agustinus Theodorus
Agustinus Theodorus

Written by Agustinus Theodorus

Loves to share his thoughts and opinions on the internet.

No responses yet