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.