DEV Community

Jay McDoniel for NestJS

Posted on • Edited on

Providing Providers to Dynamic NestJS Modules

Jay is a member of the NestJS core team, primarily helping out the community on Discord and Github and contributing to various parts of the framework.

If you've been working with NestJS for a while, you've probably heard of dynamic modules. If you're rather new to them, the docs do a pretty good job explaining them, and there's an awesome article by John about them as well that I would highly encourage reading.

I'm going to skip over the basics of dynamic modules, as the above links do a great job of explaining the concepts around them, and I'm going to be jumping into an advanced concept of providing a provider to a dynamic module. Let's unpack that sentence for a moment: we are wanting to call a dynamic module's registration method and provide a service to use instead of the default service the dynamic module already has.

The Use Case

Let's go with the general use case of having a general AuthModule with an AuthService that injects USER_SERVICE, to allow for swapping between database types (mongo, typeorm, neo4j, raw sql, etc). In doing this, we'd be able to publish the authentication module and let anyone make use of the package, while providing their own USER_SERVICE so long as it adheres to the interface we've defined.

The Setup

For this, I'm going to be using a package called @golevelup/nestjs-modules to help with the creation of the dynamic module. Instead of having to set up the entire forRoot and forRootAsync methods, we can extend a mixin and let the package take care of the setup for us. everything in this article will work without the package, I just like using it for the sake of simplicity. So, lets dive into setting up our AuthModule to be a dynamic module. First we need to create our injection token for the options

// src/auth.constants.ts

export const AUTH_OPTIONS = Symbol('AUTH_OPTIONS');
export const AUTH_SECRET = Symbol('AUTH_SECRET');
export const USER_SERVICE = Symbol('USER_SERVICE');

Enter fullscreen mode Exit fullscreen mode

For now, you can ignore the AUTH_SECRET and USER_SERVICE symbols, but we'll need it here in a moment. The next is to set up the AuthModule's options interface

// src/auth.interface.ts

import { UserService } from './user-service.interface';

export interface AuthModuleOptions {
  secret: string;
  userService: UserService;
}

Enter fullscreen mode Exit fullscreen mode

And the UserService interface defined as such:

// src/user-service.interface.ts

interface User {
  id: string;
  name: string;
  email: string;
}

export interface UserService {
  find: (id: string) => User;
  insert: (user: Exclude<User, 'id'>) => User;
}

Enter fullscreen mode Exit fullscreen mode

Now, to make our AuthModule we can simply use the createConfigurableDynamicModule method like so:

// src/auth.module.ts

import { createConfigurableDynamicRootModule } from '@golevelup/nestjs-modules';
import { Module } from '@nestjs/common';

import { AUTH_OPTIONS } from './auth.constants';
import { AuthModuleOptions } from './auth.interface';
import { AuthService } from './auth.service';

@Module({
  providers: [AuthService],
})
export class AuthModule extends createConfigurableDynamicRootModule<AuthModule, AuthModuleOptions>(AUTH_OPTIONS) {}

Enter fullscreen mode Exit fullscreen mode

And just like that, the module now has a forRoot, a forRootAsync, and a externallyConfigured static method that can all be taken advantage of (for more on the externallyConfigured method, take a look at the package's docs).

The Solution

So now, how do we ensure that users can pass in a UserService of their own, and how does our AuthService make use of it? Well, let's say that we have the following AuthService

// src/auth.service.ts

import { Injectable, Inject } from '@nestjs/common';
import { sign, verify } from 'jsonwebtoken';

import { AUTH_SECRET, USER_SERVICE } from './auth.constants';
import { UserService } from './user-service.interface';

@Injectable()
export class AuthService {
  constructor(
    @Inject(AUTH_SECRET) private readonly secret: string,
    @Inject(USER_SERVICE) private readonly userService: UserService,
  ) {}

  findUser(id: string) {
    return this.userService.find(id);
  }

  signToken(payload: Record<string, any>) {
    return sign(payload, this.secret);
  }

  verifyToken(token: string) {
    return verify(token, this.secret);
  }
}

Enter fullscreen mode Exit fullscreen mode

We have it set up to inject two providers, AUTH_SECRET and USER_SERVICE (told you they'd be needed). So now all we need to do is provide these injection tokens. But how do we do that with a dynamic module? Well, taking the module from above, we can pass in a second parameter to the createConfigurableDynamicModule method and set up providers that should exist inside the module like so

// src/auth.module.with-providers.ts

import { createConfigurableDynamicRootModule } from '@golevelup/nestjs-modules';
import { Module } from '@nestjs/common';

import { AUTH_OPTIONS, AUTH_SECRET, USER_SERVICE } from './auth.constants';
import { AuthModuleOptions } from './auth.interface';
import { AuthService } from './auth.service';

@Module({
  providers: [AuthService],
})
export class AuthModule extends createConfigurableDynamicRootModule<AuthModule, AuthModuleOptions>(AUTH_OPTIONS, {
  providers: [
    {
      provide: AUTH_SECRET,
      inject: [AUTH_OPTIONS],
      useFactory: (options: AuthModuleOptions) => options.secret,
    },
    {
      provide: USER_SERVICE,
      inject: [AUTH_OPTIONS],
      useFactory: (options: AuthModuleOptions) => options.userService,
    },
  ],
}) {}

Enter fullscreen mode Exit fullscreen mode

Using this approach, we are able to make use of the options passed in at the forRoot/forRootAsync level, while still allowing for injection of separate providers in the AuthService. Making use of this AuthModule would look something like the below:

// src/app.module.ts

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';

import { AuthModule } from './auth';
import { UserModule, UserService } from './user';

@Module({
  imports: [
    AuthModule.forRootAsync({
      imports: [ConfigModule, UserModule],
      inject: [ConfigService, UserService],
      useFactory: (config: ConfigService, userService: UserService) => {
        return {
          secret: config.get('AUTH_SECRET_VALUE'),
          userService,
        };
      },
    }),
  ],
})
export class AppModule {}

Enter fullscreen mode Exit fullscreen mode

This approach can work in many different contexts, and allows for a nice separation of all the options into smaller, more injectable sub-sets of options, which is what I do in my OgmaModule.

Top comments (10)

Collapse
 
koakh profile image
Mário Monteiro

Awesome post!
after fight some days to play with dynamic modules, and figure it out with the kind support from Jay, he post this with great tutorial

For me the point part here is the createConfigurableDynamicModule and @golevelup/nestjs-modules, this really boost the dynamic module to another level

My opinion: This post is not clear without read John and related dynamic module links, or some knowledge about dynamic modules

I don't use this new strategy yet, with createConfigurableDynamicModule, but I will play with it ASAP

If I have time next week I create a lerna repo with a standalone library and consumer app, with Jwt module (where I lost so many hours to export and import providers here) and a inMemory UserService, nothing production, only a simple repo to be easy for others to play with, obvious if Jay don't create the repo.
Thanks Jay, John and all Nest community

Collapse
 
jmcdo29 profile image
Jay McDoniel

Right, I agree that the post probably isn't clear without knowledge of Dynamic modules, which is why I linked specifically to the docs and to John's post about them, so that I could get to the meat of this tutorial. It really is important to understand the process of how dynamic modules work so that you can understand how this pattern allows for the provided providers to work.

Collapse
 
koakh profile image
Mário Monteiro

Sure, im only reinforce the idea for others to know from a newbie perspective like me :)

Collapse
 
stefaanv profile image
Stefaan Vandevelde

There might be some typo's in the last code block.
The .forRootAsync() method expects totally different parameters then the once you propose.

Collapse
 
jmcdo29 profile image
Jay McDoniel

Ah, you're right. I forgot that the default of createConfigurableDynamicModule expects the module's name to be passed as the first parameter. I'll get that fixed

Collapse
 
koakh profile image
Mário Monteiro • Edited

for other noobs like me, we need to add AuthModule ex AuthModule.forRootAsync(AuthModule

@Module({
  imports: [
    AuthModule.forRootAsync(AuthModule, {
      imports: [ConfigModule, UserModule],
      inject: [ConfigService, UserService],
      useFactory: (config: ConfigService, userService: UserService) => {
        return {
          secret: config.get('AUTH_SECRET_VALUE'),
          userService,
        };
      },
    }),
  ],
})
Enter fullscreen mode Exit fullscreen mode
Collapse
 
markpieszak profile image
Mark Pieszak

Awesome articles as always Jay! 👏

Collapse
 
koakh profile image
Mário Monteiro

Hello fellows

just to say that I create a working repo for other users to follow that awesome post, this way one can see it working out of the box and follow Jay post :)

is just a repo with a app-lib and app (consumer app), nothing fancy

feel free to create some PR's or clone, or just help me improve it

Grateful for this awesome post and framework, it's a pleasure to work with nestjs

repo link

Collapse
 
tatyanabor profile image
tatyanaBor • Edited

Hi Jay!
Maybe you know how to use dynamic modules in another dynamic modules?

For example, in your case: I want to use AuthService also as a provider in another dynamic module. How I can add AuthModule to another module in AppModule? I tried and authService is undefined.

Collapse
 
acraciel profile image
Rodrigo Rojas

Thanks for the knowledge.

Question: This can be used for an interceptor? or in that case the lifecycle is more complex and can't be usefull for that reason

cheers