Módulos dinámicos

Intermedio
Nest
Nest
Actualizado: 05/05/2026

Diagrama: tutorial-nest-dynamic-modules

Introducción a módulos dinámicos

Los módulos dinámicos en NestJS representan una evolución natural de los módulos estáticos que hemos visto anteriormente. Mientras que los módulos tradicionales tienen una configuración fija definida en tiempo de compilación, los módulos dinámicos permiten crear configuraciones flexibles que se determinan en tiempo de ejecución.

Esta capacidad resulta especialmente útil cuando necesitamos que un módulo se comporte de manera diferente según el entorno, la configuración del usuario, o parámetros específicos de la aplicación. Los módulos dinámicos son la base de muchas librerías populares del ecosistema NestJS, como @nestjs/config, @nestjs/typeorm y @nestjs/jwt.

Diferencias fundamentales con módulos estáticos

Un módulo estático tradicional se define con una estructura fija:

@Module({
  imports: [TypeOrmModule],
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService]
})
export class UserModule {}

En contraste, un módulo dinámico se crea mediante métodos estáticos que retornan un objeto DynamicModule:

@Module({})
export class DatabaseModule {
  static forRoot(options: DatabaseOptions): DynamicModule {
    return {
      module: DatabaseModule,
      imports: [],
      providers: [
        {
          provide: 'DATABASE_OPTIONS',
          useValue: options,
        },
        DatabaseService,
      ],
      exports: [DatabaseService],
    };
  }
}

Estructura de un módulo dinámico

La interfaz DynamicModule extiende las propiedades de un módulo estático añadiendo la propiedad obligatoria module:

interface DynamicModule {
  module: Type<any>;
  imports?: Array<Type<any> | DynamicModule | Promise<DynamicModule>>;
  controllers?: Type<any>[];
  providers?: Provider[];
  exports?: Array<string | symbol | Type<any> | DynamicModule>;
  global?: boolean;
}

La propiedad module debe hacer referencia a la clase del módulo que está creando la configuración dinámica. Las demás propiedades funcionan igual que en los módulos estáticos, pero pueden ser calculadas dinámicamente.

Implementación básica de un módulo dinámico

Veamos cómo crear un módulo dinámico para gestionar configuraciones de base de datos:

import { Module, DynamicModule } from '@nestjs/common';

interface DatabaseConfig {
  host: string;
  port: number;
  database: string;
  username: string;
  password: string;
}

@Module({})
export class ConfigurableModule {
  static forRoot(config: DatabaseConfig): DynamicModule {
    const providers = [
      {
        provide: 'DATABASE_CONFIG',
        useValue: config,
      },
      {
        provide: 'DATABASE_CONNECTION',
        useFactory: (config: DatabaseConfig) => {
          // Lógica para crear conexión basada en la configuración
          return `Conectado a ${config.host}:${config.port}/${config.database}`;
        },
        inject: ['DATABASE_CONFIG'],
      },
    ];

    return {
      module: ConfigurableModule,
      providers,
      exports: ['DATABASE_CONNECTION'],
    };
  }
}

Uso de módulos dinámicos

Para utilizar un módulo dinámico, lo importamos llamando a su método estático de configuración:

@Module({
  imports: [
    ConfigurableModule.forRoot({
      host: 'localhost',
      port: 5432,
      database: 'myapp',
      username: 'admin',
      password: 'secret123'
    })
  ],
  controllers: [AppController],
})
export class AppModule {}

Esta flexibilidad permite que el mismo módulo se configure de manera diferente en distintos entornos:

// Configuración para desarrollo
ConfigurableModule.forRoot({
  host: 'localhost',
  port: 5432,
  database: 'myapp_dev',
  username: 'dev_user',
  password: 'dev_pass'
})

// Configuración para producción
ConfigurableModule.forRoot({
  host: 'prod-server.com',
  port: 5432,
  database: 'myapp_prod',
  username: 'prod_user',
  password: process.env.DB_PASSWORD
})

Patrones comunes de nomenclatura

Los módulos dinámicos suelen seguir patrones de nomenclatura específicos para sus métodos estáticos:

  • forRoot(): Configuración global del módulo, típicamente usado en el módulo raíz de la aplicación
  • forFeature(): Configuración específica para características particulares, usado en módulos de funcionalidad
  • register(): Configuración simple sin opciones asíncronas
  • registerAsync(): Configuración asíncrona que permite inyección de dependencias
@Module({})
export class FeatureModule {
  // Configuración síncrona
  static register(options: FeatureOptions): DynamicModule {
    return {
      module: FeatureModule,
      providers: [
        { provide: 'FEATURE_OPTIONS', useValue: options }
      ]
    };
  }

  // Configuración asíncrona
  static registerAsync(options: FeatureAsyncOptions): DynamicModule {
    return {
      module: FeatureModule,
      imports: options.imports || [],
      providers: [
        {
          provide: 'FEATURE_OPTIONS',
          useFactory: options.useFactory,
          inject: options.inject || []
        }
      ]
    };
  }
}

Ventajas de los módulos dinámicos

Los módulos dinámicos ofrecen múltiples beneficios en el desarrollo de aplicaciones NestJS:

  • Reutilización: Un mismo módulo puede servir diferentes propósitos según su configuración
  • Flexibilidad: Permiten adaptar el comportamiento según el entorno o parámetros específicos
  • Mantenibilidad: Centralizan la lógica de configuración en un solo lugar
  • Escalabilidad: Facilitan la creación de librerías y paquetes configurables

Esta aproximación es fundamental para entender cómo funcionan internamente muchas de las librerías más utilizadas en el ecosistema NestJS, y proporciona las bases para crear nuestros propios módulos configurables y reutilizables.

ConfigurableModuleBuilder para módulos dinámicos modernos

NestJS 10+ incluye ConfigurableModuleBuilder, una utilidad que elimina el boilerplate de declarar forRoot, forRootAsync, register y registerAsync a mano. Genera automáticamente la clase base con todos los métodos:

import { ConfigurableModuleBuilder, Module } from '@nestjs/common';

export interface MailerOptions {
  apiKey: string;
  from: string;
  region?: string;
}

export const {
  ConfigurableModuleClass,
  MODULE_OPTIONS_TOKEN,
  ASYNC_OPTIONS_TYPE,
  OPTIONS_TYPE,
} = new ConfigurableModuleBuilder<MailerOptions>()
  .setExtras<{ isGlobal?: boolean }>(
    { isGlobal: false },
    (definition, extras) => ({ ...definition, global: extras.isGlobal }),
  )
  .build();

@Module({
  providers: [MailerService],
  exports: [MailerService],
})
export class MailerModule extends ConfigurableModuleClass {}

// Uso:
@Module({
  imports: [
    MailerModule.forRoot({ apiKey: process.env.MAIL_KEY, from: 'no-reply@app.com' }),
    MailerModule.forRootAsync({
      useFactory: (config: ConfigService) => ({ apiKey: config.get('MAIL_KEY'), from: config.get('MAIL_FROM') }),
      inject: [ConfigService],
    }),
  ],
})
export class AppModule {}

Con tres líneas de configuración tienes todos los métodos estándar tipados y un MODULE_OPTIONS_TOKEN que el servicio puede inyectar con @Inject(MODULE_OPTIONS_TOKEN). Es el patrón que usan internamente JwtModule, CacheModule y ThrottlerModule desde NestJS 10.

Validación de opciones con class-validator

En módulos críticos conviene validar las opciones recibidas para fallar pronto:

export class DatabaseOptions {
  @IsString() @IsNotEmpty() host: string;
  @IsInt() @Min(1) @Max(65535) port: number;
  @IsString() database: string;
  @IsString() username: string;
  @IsString() password: string;
}

@Module({})
export class DatabaseModule extends ConfigurableModuleClass {
  static forRoot(raw: unknown): DynamicModule {
    const options = plainToInstance(DatabaseOptions, raw);
    const errors = validateSync(options);
    if (errors.length > 0) {
      throw new Error(`Configuración de DatabaseModule inválida: ${JSON.stringify(errors)}`);
    }
    return super.forRoot(options);
  }
}

Esto evita errores silenciosos de tipo "puerto string en lugar de number" que se manifestarían al primer query, no al arranque.

Casos de uso enterprise

  • Multi-tenant: cargar configuración por cliente en forRootAsync con un useFactory que consulta una tabla tenants antes de instanciar el DataSource de TypeORM.
  • Feature flags por entorno: módulos dinámicos que reciben el conjunto de flags y exponen un servicio FeatureFlagService configurado.
  • Integraciones de terceros (Stripe, Twilio, AWS): cada cliente expone register({ apiKey, region }) y opciones específicas; el código de negocio depende solo del servicio inyectado.
  • Bibliotecas internas reutilizables: en monorepos, las librerías compartidas (@empresa/audit, @empresa/notifications) suelen exponerse como módulos dinámicos para que cada microservicio las configure como necesite.

Fuentes y referencias

Documentación oficial y recursos externos para profundizar en Nest

Documentación oficial de Nest
Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Nest es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Nest

Explora más contenido relacionado con Nest y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Comprender la diferencia entre módulos estáticos y dinámicos en NestJS. Aprender la estructura y propiedades de un módulo dinámico. Implementar un módulo dinámico básico con configuración personalizada. Utilizar módulos dinámicos en diferentes entornos mediante métodos estáticos. Conocer patrones comunes de nomenclatura y ventajas de los módulos dinámicos.