Architecture Hexagonale en TypeScript : une architecture propre pour des applications flexibles

Formateur Python en ligne pour débutants

Apprenez Python facilement sans surcharge théorique. Résolvez des tâches pratiques avec vérification automatique et écrivez du code directement dans le navigateur.

Start Course

Pourquoi l'Architecture Hexagonale n'est pas qu'un terme à la mode, mais un salut pour votre code ?

Imaginez que vous construisez une maison. Vous ne scellez pas les prises dans les murs en béton, n'est-ce pas ? Vous posez d'abord le câblage, faites le marquage, puis vous branchez soigneusement les prises. Dans le développement logiciel, c'est exactement la même chose — votre logique métier (le cœur de l'application) ne doit pas dépendre de la manière dont on communique avec elle : API REST, base de données ou files de messages.

L'Architecture Hexagonale (également appelée « Ports et Adaptateurs », ou « Architecture Propre » dans l'interprétation d'Alistair Cockburn) résout précisément ce problème. Dans cet article, nous allons voir comment implémenter cette architecture en TypeScript avec des exemples de code concrets. Vous apprendrez à séparer la logique métier de l'infrastructure, à rendre le code testable et flexible.



Qu'est-ce que l'Architecture Hexagonale en termes simples ?

Imaginez un hexagone. Au centre se trouve votre application avec sa logique métier. Sur chaque face se trouve un « port » (interface) par lequel le monde extérieur peut communiquer avec le cœur. Les adaptateurs sont des implémentations concrètes : contrôleurs, repositories, clients de services externes. Ils se connectent aux ports comme une prise dans une prise électrique.

Le principe principal : les dépendances sont dirigées vers l'intérieur. Le code externe (bases de données, frameworks) dépend du cœur, et non l'inverse. Cela permet de changer de base de données, par exemple de PostgreSQL à MongoDB, sans toucher à une seule ligne de la logique métier.



Concepts clés

  • Cœur (Domain) — la logique métier, les entités, les value objects, les cas d'utilisation. Aucune dépendance externe.
  • Port (Port) — une interface (en TypeScript, une classe abstraite ou une interface) qui définit un contrat pour le monde extérieur.
  • Adaptateur (Adapter) — une implémentation concrète du port. Par exemple, un repository qui fonctionne avec PostgreSQL via TypeORM.
  • Application — la couche qui coordonne l'exécution des cas d'utilisation.


Exemple pratique : système de gestion des commandes

Concevons un système de commandes simple. Nous aurons les entités Order et Product, le cas d'utilisation CreateOrder, un port pour le repository et un adaptateur pour PostgreSQL.



Étape 1. Définir le cœur (couche Domain)

Créons un dossier domain/. Les entités et les interfaces pour les ports y résident. Aucune dépendance vis-à-vis des frameworks !



// domain/order.entity.tsexport class Order {    constructor(        public readonly id: string,        public items: OrderItem[],        public status: 'pending' | 'confirmed' | 'shipped',        public total: number    ) {}

public addItem(productId: string, quantity: number, price: number): void { this.items.push({ productId, quantity, price }); this.total = this.calculateTotal(); }

private calculateTotal(): number { return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0); }}

export interface OrderItem { productId: string; quantity: number; price: number;}


Définissons maintenant le port pour le repository :



// domain/ports/order.repository.interface.tsimport { Order } from '../order.entity';

export interface IOrderRepository { save(order: Order): Promise<void>; findById(id: string): Promise<Order | null>; findAll(): Promise<Order[]>;}


Étape 2. Cas d'utilisation — le cœur de l'application

Un cas d'utilisation est un scénario d'utilisation. Il contient la logique métier et utilise les ports, mais ne sait pas qui les implémente.



// application/use-cases/create-order.use-case.tsimport { Order } from '../../domain/order.entity';import { IOrderRepository } from '../../domain/ports/order.repository.interface';

export class CreateOrderUseCase { constructor(private readonly orderRepository: IOrderRepository) {}

async execute(use

Blogs

Book Recommendations