Patrons de développement NestJS
Patrons NestJS de qualité production pour des backends TypeScript modulaires.
Quand activer
- Construction d'API ou de services NestJS
- Structuration de modules, contrôleurs et fournisseurs
- Ajout de validation DTO, gardes, intercepteurs ou filtres d'exception
- Configuration de paramètres adaptés à l'environnement et d'intégrations de bases de données
- Test de modules NestJS ou d'endpoints HTTP
Structure du projet
src/
├── app.module.ts
├── main.ts
├── common/
│ ├── filters/
│ ├── guards/
│ ├── interceptors/
│ └── pipes/
├── config/
│ ├── configuration.ts
│ └── validation.ts
├── modules/
│ ├── auth/
│ │ ├── auth.controller.ts
│ │ ├── auth.module.ts
│ │ ├── auth.service.ts
│ │ ├── dto/
│ │ ├── guards/
│ │ └── strategies/
│ └── users/
│ ├── dto/
│ ├── entities/
│ ├── users.controller.ts
│ ├── users.module.ts
│ └── users.service.ts
└── prisma/ ou database/
- Conserver le code métier dans les modules fonctionnels.
- Placer les filtres, décorateurs, gardes et intercepteurs transversaux dans
common/. - Garder les DTOs proches du module qui les possède.
Bootstrap et validation globale
async function bootstrap() {
const app = await NestFactory.create(AppModule, { bufferLogs: true });
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
transformOptions: { enableImplicitConversion: true },
}),
);
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
- Toujours activer
whitelistetforbidNonWhitelistedsur les API publiques. - Préférer un seul pipe de validation global plutôt que de répéter la configuration de validation par route.
Modules, contrôleurs et fournisseurs
@Module({
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get(':id')
getById(@Param('id', ParseUUIDPipe) id: string) {
return this.usersService.getById(id);
}
@Post()
create(@Body() dto: CreateUserDto) {
return this.usersService.create(dto);
}
}
@Injectable()
export class UsersService {
constructor(private readonly usersRepo: UsersRepository) {}
async create(dto: CreateUserDto) {
return this.usersRepo.create(dto);
}
}
- Les contrôleurs doivent rester légers : analyser l'entrée HTTP, appeler un fournisseur, retourner des DTOs de réponse.
- Mettre la logique métier dans des services injectables, pas dans les contrôleurs.
- Exporter uniquement les fournisseurs dont les autres modules ont réellement besoin.
DTOs et validation
export class CreateUserDto {
@IsEmail()
email!: string;
@IsString()
@Length(2, 80)
name!: string;
@IsOptional()
@IsEnum(UserRole)
role?: UserRole;
}
- Valider chaque DTO de requête avec
class-validator. - Utiliser des DTOs de réponse dédiés ou des sérialiseurs plutôt que de retourner directement les entités ORM.
- Éviter de divulguer des champs internes tels que les hachages de mot de passe, les jetons ou les colonnes d'audit.
Authentification, gardes et contexte de requête
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
@Get('admin/report')
getAdminReport(@Req() req: AuthenticatedRequest) {
return this.reportService.getForUser(req.user.id);
}
- Garder les stratégies d'authentification et les gardes locaux aux modules sauf s'ils sont vraiment partagés.
- Coder les règles d'accès grossières dans les gardes, puis effectuer l'autorisation spécifique aux ressources dans les services.
- Préférer des types explicites pour les objets de requête authentifiés.
Filtres d'exception et forme des erreurs
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const response = host.switchToHttp().getResponse<Response>();
const request = host.switchToHttp().getRequest<Request>();
if (exception instanceof HttpException) {
return response.status(exception.getStatus()).json({
path: request.url,
error: exception.getResponse(),
});
}
return response.status(500).json({
path: request.url,
error: 'Erreur interne du serveur',
});
}
}
- Garder une enveloppe d'erreur cohérente dans toute l'API.
- Lever les exceptions du framework pour les erreurs client attendues ; journaliser et encapsuler les échecs inattendus de manière centralisée.
Configuration et validation de l'environnement
ConfigModule.forRoot({
isGlobal: true,
load: [configuration],
validate: validateEnv,
});
- Valider l'environnement au démarrage, pas paresseusement à la première requête.
- Garder l'accès à la configuration derrière des helpers typés ou des services de configuration.
- Séparer les préoccupations dev/staging/prod dans les fabriques de configuration plutôt que de bifurquer dans le code fonctionnel.
Persistance et transactions
- Maintenir le code du dépôt/ORM derrière des fournisseurs qui parlent le langage du domaine.
- Pour Prisma ou TypeORM, isoler les workflows transactionnels dans des services qui possèdent l'unité de travail.
- Ne pas laisser les contrôleurs coordonner des écritures multi-étapes directement.
Tests
describe('UsersController', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [UsersModule],
}).compile();
app = moduleRef.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
await app.init();
});
});
- Tester unitairement les fournisseurs en isolation avec des dépendances simulées.
- Ajouter des tests au niveau des requêtes pour les gardes, les pipes de validation et les filtres d'exception.
- Réutiliser les mêmes pipes/filtres globaux dans les tests que ceux utilisés en production.
Valeurs par défaut pour la production
- Activer la journalisation structurée et les identifiants de corrélation de requête.
- Arrêter l'application en cas d'environnement/config invalide plutôt que de démarrer partiellement.
- Préférer l'initialisation asynchrone des fournisseurs pour les clients de base de données/cache avec des vérifications de santé explicites.
- Garder les tâches d'arrière-plan et les consommateurs d'événements dans leurs propres modules, pas à l'intérieur des contrôleurs HTTP.
- Rendre explicite la limitation de débit, l'authentification et la journalisation d'audit pour les endpoints publics.