En esta página

Módulos y estructura de la aplicación

15 min lectura TextoCap. 1 — Fundamentos de NestJS

El sistema de módulos de NestJS

Los módulos son el corazón de la arquitectura de NestJS. Cada aplicación tiene al menos un módulo —el módulo raíz— y la inmensa mayoría de las aplicaciones reales tienen docenas de módulos, uno por cada dominio de funcionalidad de negocio.

Un módulo en NestJS es simplemente una clase decorada con @Module(). Este decorador acepta un objeto de metadatos que describe las relaciones del módulo con el resto de la aplicación.

La anatomía del decorador @Module

@Module({
  imports: [],     // módulos externos cuyos providers necesitamos
  controllers: [], // controladores que pertenecen a este módulo
  providers: [],   // servicios, repositorios y otros providers del módulo
  exports: [],     // providers que otros módulos podrán inyectar
})

Cada propiedad tiene un propósito específico:

imports: Lista de módulos que este módulo necesita. Cuando importas un módulo, todos sus exports quedan disponibles para ser inyectados en los providers de este módulo. Esta es la forma en que los módulos se comunican entre sí.

controllers: Lista de controladores que manejan las rutas HTTP de este módulo. Los controladores son instanciados por NestJS y sus dependencias son resueltas por el contenedor de IoC.

providers: Lista de providers (servicios, repositorios, factories, etc.) que serán instanciados por el contenedor de IoC de NestJS y que pueden ser inyectados dentro de este módulo.

exports: Subconjunto de providers que este módulo pone a disposición de otros módulos. Si un módulo no exporta un provider, ese provider es privado y solo puede usarse dentro del mismo módulo.

Módulos de funcionalidad (Feature Modules)

La práctica recomendada en NestJS es crear un módulo por cada dominio de funcionalidad de tu aplicación. Si estás construyendo una tienda en línea, tendrás módulos como ProductosModule, UsuariosModule, PedidosModule, PagosModule, etc.

Creando un módulo de funcionalidad con el CLI:

nest generate module productos
nest generate controller productos
nest generate service productos

O de forma abreviada:

nest g mo productos
nest g co productos
nest g s productos

El CLI creará automáticamente la carpeta productos/ y actualizará el AppModule para importar el nuevo ProductosModule.

El árbol de módulos

NestJS construye un árbol de módulos en tiempo de arranque. El AppModule es la raíz de ese árbol. Cuando NestJS inicializa la aplicación, recorre el árbol de módulos en profundidad para instanciar todos los providers en el orden correcto, respetando las dependencias.

Este árbol tiene una propiedad importante: el scope. Un provider instanciado en el UsuariosModule solo es accesible dentro de ese módulo, a menos que sea exportado. Esto crea encapsulación real —como si cada módulo fuera un mini-aplicación con su propio contenedor de IoC.

Módulos compartidos

En aplicaciones reales, varios módulos suelen necesitar los mismos services. Por ejemplo, un LoggerService podría ser necesario en UsuariosModule, ProductosModule y PedidosModule. La solución es crear un módulo compartido:

// shared/logger/logger.module.ts
import { Module } from '@nestjs/common';
import { LoggerService } from './logger.service';

@Module({
  providers: [LoggerService],
  exports: [LoggerService],
})
export class LoggerModule {}

Cualquier módulo que importe LoggerModule podrá inyectar LoggerService. Lo importante es que NestJS reutiliza la misma instancia del módulo a lo largo del árbol de módulos —el módulo no se reinstancia en cada importación.

Módulos globales (@Global)

Cuando un provider necesita estar disponible en toda la aplicación sin necesidad de importar el módulo en cada lugar, puedes usar el decorador @Global:

import { Global, Module } from '@nestjs/common';
import { ConfigService } from './config.service';

@Global()
@Module({
  providers: [ConfigService],
  exports: [ConfigService],
})
export class ConfigModule {}

Con @Global, solo necesitas importar ConfigModule una vez —en el AppModule— y ConfigService estará disponible en toda la aplicación. Úsalo con moderación: hacer demasiados módulos globales puede hacer que las dependencias sean difíciles de rastrear.

Módulos dinámicos

Los módulos dinámicos permiten crear módulos configurables que aceptan opciones en tiempo de importación. Este patrón es muy común en librerías de NestJS como @nestjs/config, @nestjs/jwt y @nestjs/typeorm.

La convención establece tres métodos estáticos:

  • forRoot(options): para configuración global (normalmente en AppModule)
  • forFeature(options): para configuración específica de un módulo de funcionalidad
  • register(options): para módulos que se usan múltiples veces con distintas configuraciones
// emails/email.module.ts
import { DynamicModule, Module } from '@nestjs/common';
import { EmailService } from './email.service';

export interface EmailConfig {
  host: string;
  port: number;
  user: string;
  password: string;
}

@Module({})
export class EmailModule {
  static register(config: EmailConfig): DynamicModule {
    return {
      module: EmailModule,
      providers: [
        {
          provide: 'EMAIL_CONFIG',
          useValue: config,
        },
        EmailService,
      ],
      exports: [EmailService],
    };
  }
}

// En app.module.ts
@Module({
  imports: [
    EmailModule.register({
      host: 'smtp.gmail.com',
      port: 587,
      user: process.env['EMAIL_USER'] ?? '',
      password: process.env['EMAIL_PASS'] ?? '',
    }),
  ],
})
export class AppModule {}

Lazy loading de módulos

En aplicaciones muy grandes o en contextos serverless donde el tiempo de arranque es crítico, puedes cargar módulos de forma diferida usando el LazyModuleLoader:

import { Injectable } from '@nestjs/common';
import { LazyModuleLoader } from '@nestjs/core';

@Injectable()
export class AppService {
  constructor(private readonly lazyModuleLoader: LazyModuleLoader) {}

  async getReporteComplejo() {
    const { ReportesModule } = await import('./reportes/reportes.module');
    const moduleRef = await this.lazyModuleLoader.load(() => ReportesModule);
    const { ReportesService } = await import('./reportes/reportes.service');
    const reportesService = moduleRef.get(ReportesService);
    return reportesService.generarReporte();
  }
}

Re-exportando módulos

Un módulo puede re-exportar módulos que importa, actuando como un módulo de "re-exportación" o "facade". Esto es útil para crear módulos de utilidades que agregan varios módulos pequeños:

@Module({
  imports: [LoggerModule, CacheModule, DatabaseModule],
  exports: [LoggerModule, CacheModule, DatabaseModule], // re-exporta los tres
})
export class CoreModule {}

Ahora cualquier módulo que importe CoreModule obtendrá acceso a los exports de los tres módulos internos.

Estructura de carpetas recomendada

Para un proyecto NestJS mediano o grande, esta estructura de carpetas escala bien:

src/
├── common/              # Utilities, guards, interceptors compartidos
│   ├── decorators/
│   ├── filters/
│   ├── guards/
│   └── interceptors/
├── config/              # Configuración de la aplicación
├── database/            # Módulo de base de datos
├── usuarios/            # Feature module: usuarios
│   ├── dto/
│   ├── entities/
│   ├── usuarios.controller.ts
│   ├── usuarios.module.ts
│   └── usuarios.service.ts
├── productos/           # Feature module: productos
│   ├── dto/
│   ├── entities/
│   ├── productos.controller.ts
│   ├── productos.module.ts
│   └── productos.service.ts
├── app.module.ts
└── main.ts

Esta estructura garantiza que cada dominio de negocio está perfectamente encapsulado y puede evolucionar de forma independiente. En la próxima lección exploraremos los controladores —la capa que expone tu API al mundo exterior.

Convención de nombres de módulos
Cada módulo de funcionalidad debe vivir en su propia carpeta con el nombre del recurso en plural: `usuarios/`, `productos/`, `pedidos/`. Dentro de cada carpeta encontrarás el módulo, controlador, servicio y DTOs correspondientes. Esta convención hace el proyecto predecible y fácil de navegar.
Cuidado con las dependencias circulares
Si el módulo A importa al módulo B y el módulo B importa al módulo A, NestJS lanzará un error de dependencia circular. Solución: usa `forwardRef(() => ModuloB)` en la referencia circular, o mejor aún, refactoriza el código para extraer la dependencia compartida a un tercer módulo.