# Módulos y Dependency Injection Avanzada en NestJS

## forwardRef() — dependencias circulares

Usar `forwardRef()` es la **última opción**, no la primera. Antes de usarlo, preguntarse:
¿hay un diseño mejor que elimine la circularidad? Lo más común es extraer la lógica
compartida a un tercer módulo o servicio.

```typescript
// SOLO cuando la circularidad es inevitable (ej: UsersModule ↔ AuthModule)
@Module({
  imports: [forwardRef(() => AuthModule)],
  exports: [UsersService],
})
export class UsersModule {}

// En el servicio también se necesita forwardRef en el constructor
@Injectable()
export class UsersService {
  constructor(
    @Inject(forwardRef(() => AuthService))
    private readonly authService: AuthService,
  ) {}
}
```

**Cuándo NO usar forwardRef():**
- Cuando el módulo B solo necesita un tipo del módulo A (extraer a un módulo compartido)
- Cuando ambos módulos hacen cosas muy similares (unificarlos)
- Cuando la dependencia es solo en una dirección (reorganizar imports)

---

## Módulos globales (@Global)

```typescript
// Solo para módulos de infraestructura usados en TODA la app
@Global()
@Module({
  imports: [
    TypeOrmModule.forRoot({ /* ... */ }),
  ],
  providers: [DatabaseService],
  exports: [DatabaseService],
})
export class DatabaseModule {}
```

**Regla**: registrar módulos globales solo en `AppModule`. Nunca en módulos de dominio.
Los candidatos típicos son: DatabaseModule, ConfigModule, LoggerModule, CacheModule.

---

## Dynamic modules: forRoot, forRootAsync, register

El patrón `forRoot` es para configuración a nivel de aplicación (una sola vez).
El patrón `register` es para configuración a nivel de módulo (por instancia).

```typescript
// forRoot con opciones síncronas
@Module({})
export class NotificationsModule {
  static forRoot(options: NotificationsOptions): DynamicModule {
    return {
      module: NotificationsModule,
      providers: [
        { provide: NOTIFICATIONS_OPTIONS, useValue: options },
        NotificationsService,
      ],
      exports: [NotificationsService],
      global: true,
    };
  }

  // forRootAsync para opciones que dependen de ConfigService
  static forRootAsync(options: {
    inject?: (InjectionToken | OptionalFactoryDependency)[];
    useFactory: (...args: unknown[]) => NotificationsOptions | Promise<NotificationsOptions>;
  }): DynamicModule {
    return {
      module: NotificationsModule,
      providers: [
        {
          provide: NOTIFICATIONS_OPTIONS,
          useFactory: options.useFactory,
          inject: options.inject ?? [],
        },
        NotificationsService,
      ],
      exports: [NotificationsService],
      global: true,
    };
  }
}

// Uso en AppModule
@Module({
  imports: [
    NotificationsModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        apiKey: config.getOrThrow<string>('NOTIFICATIONS_KEY'),
        channel: config.get<string>('NOTIFICATIONS_CHANNEL') ?? 'email',
      }),
    }),
  ],
})
export class AppModule {}
```

---

## Injection scopes: DEFAULT vs REQUEST vs TRANSIENT

```typescript
import { Injectable, Scope } from '@nestjs/common';

// DEFAULT (singleton) — una instancia compartida en toda la app
// Usar para: servicios stateless, repositorios, clientes HTTP
@Injectable()
export class UsersService {}

// REQUEST — nueva instancia por cada request HTTP
// Usar para: servicios que necesitan acceso al request actual (ej: contexto de usuario)
// CUIDADO: degrada rendimiento por instanciación frecuente
@Injectable({ scope: Scope.REQUEST })
export class AuditService {
  constructor(@Inject(REQUEST) private readonly request: Request) {}
}

// TRANSIENT — nueva instancia cada vez que se inyecta
// Usar raramente: para servicios con estado que no deben compartirse
@Injectable({ scope: Scope.TRANSIENT })
export class TemporalStateService {}
```

**Regla práctica**: usar DEFAULT siempre. Solo cambiar a REQUEST si necesitas
el objeto `Request` dentro del servicio y no quieres pasarlo como parámetro.
Nunca TRANSIENT sin un caso de uso muy concreto.

---

## Custom providers: useClass, useValue, useFactory, useExisting

```typescript
// useValue — para constantes, configuración, mocks en tests
const CONFIG_PROVIDER = {
  provide: 'APP_CONFIG',
  useValue: { maxRetries: 3, timeout: 5000 } satisfies AppConfig,
};

// useClass — para intercambiar implementaciones (útil en tests)
const MAILER_PROVIDER = {
  provide: MailerService,
  useClass: process.env['NODE_ENV'] === 'test'
    ? MockMailerService
    : SmtpMailerService,
};

// useFactory — para inicialización asíncrona con dependencias
const REDIS_PROVIDER = {
  provide: 'REDIS_CLIENT',
  inject: [ConfigService],
  useFactory: async (config: ConfigService) => {
    const client = createClient({
      url: config.getOrThrow<string>('REDIS_URL'),
    });
    await client.connect();
    return client;
  },
};

// useExisting — alias de un provider existente
const ALIAS_PROVIDER = {
  provide: 'LEGACY_USERS_SERVICE',
  useExisting: UsersService,
};

// Uso en módulo
@Module({
  providers: [
    CONFIG_PROVIDER,
    MAILER_PROVIDER,
    REDIS_PROVIDER,
    ALIAS_PROVIDER,
  ],
})
export class InfraModule {}
```

---

## ConfigModule con validación Joi al arranque

```typescript
// config/configuration.ts
export const configuration = () => ({
  port: parseInt(process.env['PORT'] ?? '3000', 10),
  database: {
    url: process.env['DATABASE_URL'],
    ssl: process.env['DATABASE_SSL'] === 'true',
  },
  jwt: {
    secret: process.env['JWT_SECRET'],
    expiresIn: process.env['JWT_EXPIRES_IN'] ?? '7d',
  },
});

export type AppConfig = ReturnType<typeof configuration>;
```

```typescript
// app.module.ts
import * as Joi from 'joi';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      load: [configuration],
      validationSchema: Joi.object({
        PORT: Joi.number().default(3000),
        DATABASE_URL: Joi.string().uri().required(),
        JWT_SECRET: Joi.string().min(32).required(),
        JWT_EXPIRES_IN: Joi.string().default('7d'),
        NODE_ENV: Joi.string()
          .valid('development', 'production', 'test')
          .default('development'),
      }),
      validationOptions: {
        abortEarly: false,  // Reportar todos los errores, no solo el primero
      },
    }),
  ],
})
export class AppModule {}
```

La validación ocurre al inicio: si falta `DATABASE_URL` o `JWT_SECRET`,
la aplicación no arranca. Mejor fallar rápido que fallar en runtime.

---

## Prisma como provider alternativo a TypeORM

```typescript
// prisma/prisma.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService
  extends PrismaClient
  implements OnModuleInit, OnModuleDestroy
{
  async onModuleInit(): Promise<void> {
    await this.$connect();
  }

  async onModuleDestroy(): Promise<void> {
    await this.$disconnect();
  }
}

// prisma/prisma.module.ts
@Global()
@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

// Uso en un servicio — sin @InjectRepository, sin forFeature
@Injectable()
export class UsersService {
  constructor(private readonly prisma: PrismaService) {}

  async obtenerPorId(id: string): Promise<User> {
    const usuario = await this.prisma.user.findUnique({ where: { id } });
    if (!usuario) throw new NotFoundException(`Usuario ${id} no encontrado`);
    return usuario;
  }
}
```

Prisma es la opción preferida para proyectos nuevos: mejor DX, tipos generados
automáticamente desde el schema, migraciones más predecibles que TypeORM.
