Advanced NestJS: Dynamic ProvidersAdvanced NestJS: Dynamic Providers
Livio is a member of the NestJS core team and creator of the @nestjs/terminus integration
Intro
Dependency Injection (short DI) is a powerful technique to build a loosely coupled architecture in a testable manner. In NestJS an item which is part of the DI context is called provider. A provider consists of two main parts, a value, and a unique token. In NestJS you can request the value of a provider by its token. This is most apparent when using the following snippet.
import { NestFactory } from '@nestjs/core';
import { Module } from '@nestjs/common';
@Module({
providers: [
{
provide: 'PORT',
useValue: 3000,
},
],
})
export class AppModule {}
async function bootstrap() {
const app = await NestFactory.createApplicationContext(AppModule);
const port = app.get('PORT');
console.log(port); // Prints: 3000
}
bootstrap();
The AppModule
consists of one provider with the token PORT
.
- We bootstrap our application by calling
NestFactory.createApplicationContext
(this method does the same asNestFactory.create
but does not initiate an HTTP instance). - Later on, we request the value of our provider with
app.get('PORT')
. This will return3000
as specified in our provider.
Fair enough. But what if you do not know what you will provide to the user? What if you need to compute the providers during runtime?
This article goes into a technique which we use often for various NestJS integrations. This technique will allow you to build highly dynamic NestJS applications and still make use of the advantages of DI.
What are we trying to achieve
To see use cases of dynamic providers we will use a simple but useful example. We want to have a parameter decorator called Logger
which takes an optional prefix
as string
. This decorator will inject a LoggerService
, which prepends the given prefix
to every log message.
So the final implementation will look like this:
@Injectable()
export class AppService {
constructor(@Logger('AppService') private logger: LoggerService) {}
getHello() {
this.logger.log('Hello World'); // Prints: '[AppService] Hello World'
return 'Hello World';
}
}
Setup a NestJS application
We will make use of the NestJS CLI to get started quickly. If you have not installed it, use the following command:
npm i -g @nestjs/cli
Now run the following command in your terminal of choice to bootstrap your Nest application.
nest new logger-app && cd logger-app
Logger service
Let's start off with our LoggerService
. This service will get injected later when we use our @Logger()
decorator. Our basic requirements for this service are:
- A method which can log messages to stdout
- A method which can set the prefix of each instance
Once again we will use the NestJS CLI to bootstrap our module and service.
nest generate module Logger
nest generate service Logger
To satisfy our requirements we build this minimal LoggerService
.
// src/logger/logger.service.ts
import { Injectable, Scope } from '@nestjs/common';
@Injectable({
scope: Scope.TRANSIENT,
})
export class LoggerService {
private prefix?: string;
log(message: string) {
let formattedMessage = message;
if (this.prefix) {
formattedMessage = `[${this.prefix}] ${message}`;
}
console.log(formattedMessage);
}
setPrefix(prefix: string) {
this.prefix = prefix;
}
}
First of all, you may have realized that the @Injectable()
decorator uses the scope option with Scope.TRANSIENT
. This basically means every time the LoggerService
gets injected in our application, it will create a new instance of the class. This is mandatory due to the prefix
attribute. We do not want to have a single instance of the LoggerService
and constantly override the prefix
option.
Other than that, the LoggerService
should be self-explanatory.
Now we only have to export our service in the LoggerModule
, so we can use it in AppModule
.
// src/logger/logger.module.ts
import { Module } from '@nestjs/common';
import { LoggerService } from './logger.service';
@Module({
providers: [LoggerService],
exports: [LoggerService],
})
export class LoggerModule {}
Let's see if it works in our AppService
.
// src/app.service.ts
import { Injectable } from '@nestjs/common';
import { LoggerService } from './logger/logger.service';
@Injectable()
export class AppService {
constructor(private readonly logger: LoggerService) {
this.logger.setPrefix('AppService');
}
getHello(): string {
this.logger.log('Hello World');
return 'Hello World!';
}
}
Seems fine - let's start the application with npm run start
and request the website with curl http://localhost:3000/
or open up http://localhost:3000
in your browser of choice.
If everything is set up correctly we will receive the following log output.
[AppService] Hello World
That is cool. Though, we are lazy, aren't we? We do not want to explicitly write this.logger.setPrefix('AppService')
in the constructor of our services? Something like @Logger('AppService')
before our logger
-parameter would be way less verbose and we would not have to define a constructor every time we want to use our logger.
Logger Decorator
For our example, we do not need to exactly know how decorators work in TypeScript. All you need to know is that functions can be handled as a decorator.
Lets quickly create our decorator manually.
touch src/logger/logger.decorator.ts
We are just going to reuse the @Inject()
decorator from @nestjs/common
.
// src/logger/logger.decorator.ts
import { Inject } from '@nestjs/common';
export const prefixesForLoggers: string[] = new Array<string>();
export function Logger(prefix: string = '') {
if (!prefixesForLoggers.includes(prefix)) {
prefixesForLoggers.push(prefix);
}
return Inject(`LoggerService${prefix}`);
}
You can think of @Logger('AppService')
as nothing more than an alias for @Inject('LoggerServiceAppService')
. The only special thing we have added is the prefixesForLoggers
array. We will make use of this array later. This array just stores all the prefixes we are going to need.
But wait, our Nest application does not know anything about a LoggerServiceAppService
token. So let's create this token using dynamic providers and our newly created prefixesForLoggers
array.
Dynamic providers
In this chapter, we want to have a look at dynamically generating providers. We want to
- create a provider for each prefix
- each of these providers must have a token like this
'LoggerService' + prefix
- each provider must call
LoggerService.setPrefix(prefix)
upon its instantiation
- each of these providers must have a token like this
To implement these requirements we create a new file.
touch src/logger/logger.providers.ts
Copy & paste the following code into your editor.
// src/logger/logger.provider.ts
import { prefixesForLoggers } from './logger.decorator';
import { Provider } from '@nestjs/common';
import { LoggerService } from './logger.service';
function loggerFactory(logger: LoggerService, prefix: string) {
if (prefix) {
logger.setPrefix(prefix);
}
return logger;
}
function createLoggerProvider(prefix: string): Provider<LoggerService> {
return {
provide: `LoggerService${prefix}`,
useFactory: logger => loggerFactory(logger, prefix),
inject: [LoggerService],
};
}
export function createLoggerProviders(): Array<Provider<LoggerService>> {
return prefixesForLoggers.map(prefix => createLoggerProvider(prefix));
}
The createLoggerProviders
-function creates an array of providers for each prefix set by the @Logger()
decorator. Thanks to the useFactory
functionality of NestJS we can run a the LoggerService.setPrefix()
method before the provider gets created.
All we need to do now is to add these logger providers to our LoggerModule
.
// src/logger/logger.module.ts
import { Module } from '@nestjs/common';
import { LoggerService } from './logger.service';
import { createLoggerProviders } from './logger.providers';
const loggerProviders = createLoggerProviders();
@Module({
providers: [LoggerService, ...loggerProviders],
exports: [LoggerService, ...loggerProviders],
})
export class LoggerModule {}
As simple as that. Wait no, that does not work? Because of JavaScript, man. Let me explain: createLoggerProviders
will get called immediately once the file is loaded, right? At that point in time, the prefixesForLoggers
array will be empty inside logger.decorator.ts
, because the @Logger()
decorator was not called.
So how do we bypass that? The holy words are Dynamic Module. Dynamic modules allow us to create the module settings (which are usually given as parameter of the @Module
-decorator) via a method. This method will get called after the @Logger
decorator calls and therefore prefixForLoggers
array will contain all the values.
If you want to learn more about why this works, you may wanna check out this video about the JavaScript event loop
Therefore we have to rewrite the LoggerModule
to a Dynamic Module.
// src/logger/logger.module.ts
import { DynamicModule } from '@nestjs/common';
import { LoggerService } from './logger.service';
import { createLoggerProviders } from './logger.providers';
export class LoggerModule {
static forRoot(): DynamicModule {
const prefixedLoggerProviders = createLoggerProviders();
return {
module: LoggerModule,
providers: [LoggerService, ...prefixedLoggerProviders],
exports: [LoggerService, ...prefixedLoggerProviders],
};
}
}
Do not forget to update the import array in app.module.ts
// src/logger/app.module.ts
@Module({
controllers: [AppController],
providers: [AppService],
imports: [LoggerModule.forRoot()],
})
export class AppModule {}
...and that's it! Let's see if it works when we update the app.service.ts
// src/app.service.ts
@Injectable()
export class AppService {
constructor(@Logger('AppService') private logger: LoggerService) {}
getHello() {
this.logger.log('Hello World'); // Prints: '[AppService] Hello World'
return 'Hello World';
}
}
Calling http://localhost:3000
will give us the following log
[AppService] Hello World
Yey, we did it!
Conclusion
We have touched on numerous advanced parts of NestJS. We have seen how we can create simple decorators, dynamic modules and dynamic providers. You can do impressive stuff with it in a clean and testable way.
As mentioned we have used the exact same patterns for the internals of @nestjs/typeorm
and @nestjs/mongoose
. In the Mongoose integration, for example, we used a very similar approach for generating injectable providers for each model.
You can find the code in this Github repostiory. I have also refactored smaller functionalities and added unit tests, so you can use this code in production. Happy hacking :)