L'architecture modulaire de NestJS

Organiser le code en modules fonctionnels cohérents

Chaque module encapsule une fonctionnalité : users, auth, products, orders
 Cette organisation favorise la réutilisabilité, la testabilité et la séparation des préoccupations.

Anatomie d'un Module

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';
import { MailModule } from '../mail/mail.module';

@Module({
  imports: [
    TypeOrmModule.forFeature([User]),  // Enregistre l'entité
    MailModule,                        // Import d'un autre module
  ],
  controllers: [UsersController],      // Controllers de ce module
  providers: [UsersService],           // Services de ce module
  exports: [UsersService],             // Rend UsersService utilisable par d'autres modules
})
export class UsersModule {}

Le module racine

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    TypeOrmModule.forRoot({ ... }),  // Config DB globale
    UsersModule,
    AuthModule,
    ProductsModule,
    OrdersModule,
  ],
})
export class AppModule {}
RĂšgle d'or : Chaque module dĂ©finit ses propres providers dans providers. Pour qu'un provider soit accessible depuis un autre module, il doit ĂȘtre listĂ© dans exports.

Imports & Exports

Partager un service entre modules

// mail.module.ts — exporte MailService
@Module({
  providers: [MailService],
  exports: [MailService],  // Rendu disponible à l'extérieur
})
export class MailModule {}

// users.module.ts — importe MailModule
@Module({
  imports: [MailModule],  // AccĂšs Ă  MailService via l'export
  providers: [UsersService],
})
export class UsersModule {}

// users.service.ts — injection automatique
@Injectable()
export class UsersService {
  constructor(private readonly mailService: MailService) {}

  async create(dto: CreateUserDto) {
    const user = await this.save(dto);
    await this.mailService.sendWelcome(user.email);
    return user;
  }
}

Re-exporter un module importé

// core.module.ts — re-exporte ConfigModule pour toute l'app
@Module({
  imports: [ConfigModule.forRoot()],
  exports: [ConfigModule],  // Re-export
})
export class CoreModule {}

Modules dynamiques

Les modules dynamiques permettent de configurer un module au moment de son import, comme TypeOrmModule.forRoot(options).

// cache.module.ts
import { Module, DynamicModule } from '@nestjs/common';

interface CacheOptions {
  ttl: number;
  max: number;
}

@Module({})
export class CacheModule {
  static register(options: CacheOptions): DynamicModule {
    return {
      module: CacheModule,
      providers: [
        {
          provide: 'CACHE_OPTIONS',
          useValue: options,
        },
        CacheService,
      ],
      exports: [CacheService],
    };
  }
}

// Utilisation
@Module({
  imports: [CacheModule.register({ ttl: 300, max: 100 })],
})
export class AppModule {}

forRootAsync — avec dĂ©pendances

CacheModule.registerAsync({
  imports: [ConfigModule],
  useFactory: (config: ConfigService) => ({
    ttl: config.get<number>('CACHE_TTL'),
    max: config.get<number>('CACHE_MAX'),
  }),
  inject: [ConfigService],
})

Conteneur d'injection de dépendances

NestJS maintient un registre de tous les providers. À l'instanciation, il rĂ©sout automatiquement les dĂ©pendances.

// Le graphe de dépendances est résolu automatiquement :
// AppModule
//   └── UsersModule
//         ├── UsersController (injecte UsersService)
//         └── UsersService (injecte Repository<User>, MailService)
//               └── MailModule
//                     └── MailService (injecte ConfigService)

// Tout est résolu sans new() explicite
@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User) private usersRepo: Repository<User>,
    private mailService: MailService,  // Injecté automatiquement
  ) {}
}
Tokens d'injection : Par défaut, NestJS utilise le type TypeScript comme token. Vous pouvez aussi utiliser une chaßne ou un Symbol : @Inject('MY_TOKEN').

Providers avancés

useFactory avec async

// Connexion DB asynchrone
{
  provide: 'DATABASE_CONNECTION',
  useFactory: async (config: ConfigService): Promise<Connection> => {
    return createConnection({
      type: 'postgres',
      url: config.get('DATABASE_URL'),
    });
  },
  inject: [ConfigService],
}

useExisting — alias

// Permet d'injecter UsersService avec deux tokens différents
providers: [
  UsersService,
  { provide: IUsersService, useExisting: UsersService },
]

Injection dans la classe module elle-mĂȘme

@Module({ providers: [AppService] })
export class AppModule implements OnModuleInit {
  constructor(private appService: AppService) {}

  onModuleInit() {
    this.appService.initialize();
  }
}

Modules globaux

// Disponible partout sans import explicite
@Global()
@Module({
  providers: [LoggerService, EventEmitterService],
  exports: [LoggerService, EventEmitterService],
})
export class SharedModule {}

// Dans AppModule — suffit d'importer une fois
@Module({
  imports: [SharedModule, UsersModule, ...],
})
export class AppModule {}
Utilisez @Global() avec parcimonie. Trop de modules globaux rend les dépendances implicites et difficiles à tracer. Préférez des imports explicites sauf pour les utilitaires vraiment transversaux (logger, config, event emitter).

Dépendances circulaires

Une dépendance circulaire survient quand A dépend de B qui dépend de A. NestJS le gÚre avec forwardRef().

// users.service.ts — dĂ©pend de AuthService
@Injectable()
export class UsersService {
  constructor(
    @Inject(forwardRef(() => AuthService))
    private authService: AuthService,
  ) {}
}

// auth.service.ts — dĂ©pend de UsersService
@Injectable()
export class AuthService {
  constructor(
    @Inject(forwardRef(() => UsersService))
    private usersService: UsersService,
  ) {}
}

// Les modules aussi doivent utiliser forwardRef
@Module({
  imports: [forwardRef(() => AuthModule)],
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

@Module({
  imports: [forwardRef(() => UsersModule)],
  providers: [AuthService],
  exports: [AuthService],
})
export class AuthModule {}
Évitez les dĂ©pendances circulaires quand c'est possible. Elles signalent souvent un problĂšme de conception. Solution : extraire la logique partagĂ©e dans un troisiĂšme service.
✏ Exercices du module ▶ Mini-projet Module 04 →