How to Create Dynamic Modules in NestJS: The Complete Guide

Dynamic modules in NestJS are reusable, configurable modules created by static factory methods like register() or registerAsync() that return a DynamicModule object containing the module class plus runtime metadata such as providers, imports, and exports.

A dynamic module allows you to pass configuration into a module at the moment you import it, making the module system in the nestjs/nest repository flexible enough to handle database connections, configuration services, and third-party integrations. Unlike static modules, which have fixed metadata, dynamic modules conform to the DynamicModule interface exported from @nestjs/common and are processed by Nest's internal scanner at bootstrap time.

What Are Dynamic Modules in NestJS?

A dynamic module is a module definition object that contains a module property pointing to the module class, alongside additional metadata like providers, imports, and exports. This object is returned by a static method—conventionally named register(), forRoot(), or forFeature()—that accepts configuration arguments.

According to the NestJS source code, the DynamicModule interface is a simple contract: it requires a module property (the class reference) and allows all other standard module metadata. When you call ConfigModule.register({ folder: './config' }), you are not importing the class directly; you are invoking a factory that returns this structured object.

How NestJS Detects and Processes Dynamic Modules

The framework handles dynamic modules through a specific internal pipeline involving the scanner, compiler, and container.

Detection in the Scanner

When Nest boots, the Scanner class iterates over all module definitions to determine which are dynamic. In packages/core/scanner.ts at lines 716-719, the scanner uses the isDynamicModule utility to check for the presence of a module property:

// Simplified logic from packages/core/scanner.ts#L716-L719
if (isDynamicModule(moduleDefinition)) {
  const { module: moduleType, ...metadata } = moduleDefinition;
  // Process as dynamic module
}

If the object contains a module key, Nest extracts the class and treats the remaining properties as metadata overrides.

Compilation and Metadata Merging

After detection, the Compiler class merges the dynamic metadata into the module tree. In packages/core/injector/compiler.ts at lines 65-68, the compiler separates the dynamic module's extra providers and imports from the base module class, combining them into the final dependency graph:

// Conceptual view of packages/core/injector/compiler.ts#L65-L68
const dynamicMetadata = omit(dynamicModule, 'module');
// Merge into existing module metadata

Container Registration

The Container class stores dynamic module metadata for later resolution. In packages/core/injector/container.ts at lines 201-210, the addDynamicModules method walks the imports array, recursively registers any nested dynamic modules, and caches the partial metadata:

// From packages/core/injector/container.ts#L201-L210
async addDynamicModules(modules: any[], scope: Type<any>[]) {
  for (const moduleDef of modules) {
    const isDyn = this.isDynamicModule(moduleDef);
    const token = isDyn ? moduleDef.module : moduleDef;
    this.dynamicModuleMetadata.set(token.name, moduleDef);
    if (isDyn && moduleDef.imports) {
      await this.addDynamicModules(moduleDef.imports, scope);
    }
  }
}

Creating a Basic Dynamic Module

To implement a synchronous dynamic module, define a static register() method that returns a DynamicModule object. The following example from sample/25-dynamic-modules/src/config/config.module.ts demonstrates a configuration module that accepts a folder path:

// src/config/config.module.ts
import { DynamicModule, Module } from '@nestjs/common';
import { ConfigService } from './config.service';
import { CONFIG_OPTIONS } from './constants';

export interface ConfigModuleOptions {
  folder: string;
}

@Module({})
export class ConfigModule {
  static register(options: ConfigModuleOptions): DynamicModule {
    return {
      module: ConfigModule,
      providers: [
        {
          provide: CONFIG_OPTIONS,
          useValue: options,
        },
        ConfigService,
      ],
      exports: [ConfigService],
    };
  }
}

In this implementation:

  • The module property identifies the class that owns the metadata.
  • A custom provider injects the options object using the CONFIG_OPTIONS injection token.
  • ConfigService can now access these options via constructor injection.

Consuming the Dynamic Module

Import the module using the static factory method instead of the class reference:

// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from './config/config.module';
import { UsersModule } from './users/users.module';

@Module({
  imports: [
    ConfigModule.register({ folder: './config' }),
    UsersModule,
  ],
})
export class AppModule {}

Async Dynamic Module Registration

For asynchronous configuration—such as loading secrets from a remote vault—use the registerAsync() pattern with useFactory providers. This approach allows you to resolve dependencies asynchronously before the module is fully instantiated:

// src/database/database.module.ts
import { DynamicModule, Module } from '@nestjs/common';
import { createConnection } from 'typeorm';
import { DATABASE_OPTIONS } from './constants';

@Module({})
export class DatabaseModule {
  static registerAsync(): DynamicModule {
    return {
      module: DatabaseModule,
      imports: [],
      providers: [
        {
          provide: DATABASE_OPTIONS,
          useFactory: async () => {
            const secret = await fetchSecret('db-credentials');
            return {
              type: 'postgres',
              host: secret.host,
              username: secret.user,
              password: secret.pass,
              database: secret.db,
            };
          },
        },
        {
          provide: 'DB_CONNECTION',
          useFactory: async (opts: any) => await createConnection(opts),
          inject: [DATABASE_OPTIONS],
        },
      ],
      exports: ['DB_CONNECTION'],
    };
  }
}

The registerAsync method returns the same DynamicModule structure, but the providers use factory functions that return Promises. Nest waits for these promises to resolve before finishing the module initialization.

Advanced: Accessing Dynamic Modules at Runtime

Once registered, dynamic modules are accessible via the ModuleRef utility. In packages/core/nest-application-context.ts at lines 93-107, the select method resolves the module token to the stored dynamic metadata, allowing you to retrieve providers with the concrete values supplied at registration:

// From packages/core/nest-application-context.ts#L93-L107
select<T>(moduleType: Type<T>): T {
  const moduleRef = this.container.getModule(moduleType);
  // Returns module reference with dynamic metadata applied
}

This enables runtime introspection of dynamically configured providers within the selected module scope.

Summary

  • Dynamic modules in NestJS are created by static methods returning a DynamicModule object with a module property and runtime metadata.
  • The Scanner detects dynamic modules via isDynamicModule in packages/core/scanner.ts, while the Compiler merges metadata in packages/core/injector/compiler.ts.
  • The Container registers dynamic modules recursively through addDynamicModules in packages/core/injector/container.ts.
  • Use register() for synchronous configuration and registerAsync() for asynchronous setup involving factory providers.
  • Dynamic modules enable reusable, configurable integrations that accept parameters at import time rather than at definition time.

Frequently Asked Questions

What is the difference between register() and registerAsync() in NestJS dynamic modules?

register() is a synchronous factory method that accepts static configuration options and immediately returns a DynamicModule object with useValue providers. registerAsync() supports asynchronous operations, allowing you to use useFactory, useClass, or useExisting providers that return Promises, making it ideal for loading configuration from environment variables, remote secrets managers, or databases at bootstrap time.

How does NestJS distinguish between a static module and a dynamic module?

NestJS checks for the presence of a module property on the imported object. According to packages/core/scanner.ts#L716-L719, the internal isDynamicModule function returns true if the definition is an object containing a module key pointing to a class constructor. Static modules are class references passed directly, while dynamic modules are objects returned by static factory methods.

Can dynamic modules in NestJS import other dynamic modules?

Yes, dynamic modules can import other dynamic modules. In packages/core/injector/container.ts#L201-L210, the addDynamicModules method recursively processes the imports array of a dynamic module, detecting and registering any nested dynamic definitions. This allows you to compose complex module trees where configuration flows through multiple levels of dynamic factories.

What is the DynamicModule interface and where is it defined?

The DynamicModule interface is defined in @nestjs/common and specifies the contract for dynamic module objects. It requires a module property (the class type) and optionally accepts imports, controllers, providers, and exports. This interface allows Nest's dependency injection system to treat runtime-generated module metadata identically to static @Module() decorator metadata.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →