181 lines
6.0 KiB
Markdown
181 lines
6.0 KiB
Markdown
# 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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 `any` dans 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_SYNC` est 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)
|
|
|
|
```json
|
|
{
|
|
"@domain/*": ["src/domain/*"],
|
|
"@application/*": ["src/application/*"],
|
|
"@infrastructure/*": ["src/infrastructure/*"]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Configuration TypeScript
|
|
|
|
- `strict: true`
|
|
- `strictNullChecks: true`
|
|
- `strictPropertyInitialization: false` (exception pour ORM entities)
|
|
- `noImplicitAny: true`
|