6.0 KiB
6.0 KiB
Architecture Backend — NestJS Hexagonale
Structure des couches
apps/backend/src/
├── domain/ # Logique métier PURE — aucun import NestJS/TypeORM
│ ├── entities/ # 17 entités métier (private constructor + static create())
│ ├── value-objects/ # Objets valeur immuables (Email, Money, BookingNumber...)
│ ├── services/ # Services métier purs (csv-rate-price-calculator)
│ ├── exceptions/ # Exceptions domaine
│ └── ports/
│ ├── in/ # Interfaces use-case (execute())
│ └── out/ # Interfaces repository/SPI (token constants)
│
├── application/ # Couche API — dépend UNIQUEMENT du domain
│ ├── controllers/ # REST controllers (Swagger + class-validator)
│ ├── dto/ # Data Transfer Objects
│ ├── mappers/ # Domain ↔ DTO (méthodes statiques)
│ ├── guards/ # JwtAuthGuard, RolesGuard, ApiKeyOrJwtGuard
│ ├── decorators/ # @Public(), @Roles(), @CurrentUser()
│ ├── filters/ # DomainExceptionFilter
│ ├── interceptors/ # PerformanceMonitoringInterceptor
│ ├── gateways/ # WebSocket Socket.IO (notifications)
│ ├── services/ # Services applicatifs (audit, notification, export, webhooks)
│ └── [feature]/ # Modules par feature (auth, bookings, rates, etc.)
│
└── infrastructure/ # Adaptateurs externes — dépend UNIQUEMENT du domain
├── persistence/typeorm/
│ ├── entities/ # ORM entities (*.orm-entity.ts)
│ ├── repositories/# Implémentations TypeORM
│ ├── mappers/ # ORM ↔ Domain (static toOrm/toDomain/toDomainMany)
│ └── migrations/ # Migrations TypeORM
├── carriers/ # 5 connecteurs carriers + CSV loader
├── cache/ # Adaptateur Redis
├── email/ # MJML + Nodemailer
├── storage/ # S3/MinIO (+ csv-storage/)
├── stripe/ # Paiements et abonnements
├── external/ # Pappers (SIRET), ECU Worldwide
├── pdf/ # Génération PDF pdfkit
├── security/ # Utilitaires sécurité
└── monitoring/ # Sentry + Pino
Entités domaine (17)
| Entité | Fichier |
|---|---|
| ApiKey | api-key.entity.ts |
| AuditLog | audit-log.entity.ts |
| BlogPost | blog-post.entity.ts |
| Booking | booking.entity.ts |
| Carrier | carrier.entity.ts |
| Container | container.entity.ts |
| CsvBooking | csv-booking.entity.ts |
| CsvRate | csv-rate.entity.ts |
| InvitationToken | invitation-token.entity.ts |
| License | license.entity.ts |
| Notification | notification.entity.ts |
| Organization | organization.entity.ts |
| Port | port.entity.ts |
| RateQuote | rate-quote.entity.ts |
| Subscription | subscription.entity.ts |
| User | user.entity.ts |
| Webhook | webhook.entity.ts |
Pattern entité domaine
// Private constructor + static create() factory
export class Booking {
private constructor(private readonly props: BookingProps) {}
static create(props: Omit<BookingProps, 'bookingNumber' | 'status'>): Booking {
return new Booking({
...props,
bookingNumber: BookingNumber.generate(),
status: BookingStatus.DRAFT,
});
}
// Les mutations retournent une nouvelle instance (immuabilité)
updateStatus(newStatus: BookingStatus): Booking {
return new Booking({ ...this.props, status: newStatus });
}
}
Pattern repository
// domain/ports/out/booking.repository.ts
export const BOOKING_REPOSITORY = 'BookingRepository';
export interface BookingRepository {
findById(id: string): Promise<Booking | null>;
save(booking: Booking): Promise<Booking>;
findByOrganization(orgId: string, filters: BookingFilters): Promise<Booking[]>;
}
// infrastructure/persistence/typeorm/repositories/typeorm-booking.repository.ts
@Injectable()
export class TypeOrmBookingRepository implements BookingRepository {
constructor(
@InjectRepository(BookingOrmEntity)
private readonly repo: Repository<BookingOrmEntity>,
) {}
// ...
}
Conventions de nommage
| Couche | Suffix | Exemple |
|---|---|---|
| Domain entity | .entity.ts | booking.entity.ts |
| Value object | .vo.ts | money.vo.ts |
| In port | .use-case.ts | create-booking.use-case.ts |
| Out port | .repository.ts | booking.repository.ts |
| ORM entity | .orm-entity.ts | booking.orm-entity.ts |
| ORM mapper | .mapper.ts | booking.mapper.ts |
| Controller | .controller.ts | bookings.controller.ts |
| DTO | .dto.ts | create-booking-request.dto.ts |
| App mapper | .mapper.ts | booking.mapper.ts |
Règles critiques
- Jamais d'import NestJS/TypeORM dans
domain/ - Jamais de type
anydans le backend (strict mode) - Jamais modifier une migration déjà appliquée
- Toujours valider les DTOs avec class-validator
- Toujours créer un mapper séparé pour ORM ↔ Domain
DATABASE_SYNCest hard-codé àfalse— utiliser les migrations
Modules NestJS (app.module.ts)
Guards globaux (toutes routes) :
JwtAuthGuard— JWT obligatoire sauf routes@Public()CustomThrottlerGuard— rate limiting
Feature modules : Auth · Rates · Ports · Bookings · CsvBookings · Organizations · Users · Dashboard · Audit · Notifications · Webhooks · GDPR · Admin · Subscriptions · ApiKeys · Blog · Logs
Infrastructure modules : CacheModule · CarrierModule · SecurityModule · CsvRateModule · StripeModule · PdfModule · StorageModule · EmailModule
Alias de chemins (tsconfig.json)
{
"@domain/*": ["src/domain/*"],
"@application/*": ["src/application/*"],
"@infrastructure/*": ["src/infrastructure/*"]
}
Configuration TypeScript
strict: truestrictNullChecks: truestrictPropertyInitialization: false(exception pour ORM entities)noImplicitAny: true