Skip to content
Learni
Voir tous les tutoriels
Architecture Logicielle

Comment implémenter Clean Architecture en 2026

Read in English

Introduction

Clean Architecture, conceptualisée par Robert C. Martin (Uncle Bob), vise à créer des applications logicielles indépendantes des frameworks, bases de données ou interfaces utilisateur. Imaginez votre code comme une oignon : au cœur, les règles métier pures (entités et use cases) ; à la périphérie, les détails techniques (DB, UI). Pourquoi l'adopter en 2026 ? Elle rend vos apps testables, maintenables et évolutives. Dans ce tutoriel beginner, nous construisons une API TODO simple en TypeScript : création, lecture, mise à jour et suppression de tâches. Chaque couche dépend uniquement de l'intérieur, pas l'inverse. Résultat : un code découplé, facile à refactoriser. Prêt à transformer vos projets monolithiques en architectures solides ? (128 mots)

Prérequis

  • Node.js 20+ installé
  • Connaissances de base en TypeScript
  • Un éditeur comme VS Code
  • npm ou yarn pour les dépendances

Initialiser le projet et installer les dépendances

terminal
mkdir clean-architecture-todo
cd clean-architecture-todo
npm init -y
npm install express typescript ts-node @types/node @types/express
npm install -D @types/express
npx tsc --init
mkdir -p src/{domain,application,infrastructure,adapters,presentation}
touch src/domain/entities/Todo.ts
 touch src/domain/repositories/TodoRepository.ts

Cette commande initialise un projet Node.js avec TypeScript et Express. Nous créons la structure de dossiers Clean Architecture : domain (règles métier), application (use cases), infrastructure (implémentations), adapters (contrôleurs), presentation (frameworks). Cela sépare les préoccupations dès le départ, évitant les pièges des projets plats.

Étape 1 : Définir les entités du domaine

Les entités forment le cœur de Clean Architecture : objets métier purs, sans dépendances externes. Pour nos TODOs, une entité Todo avec id, titre, description et statut. Pas de DB ou HTTP ici – pure logique métier.

Créer l'entité Todo

src/domain/entities/Todo.ts
export interface Todo {
  id: string;
  title: string;
  description: string;
  completed: boolean;
}

export class TodoEntity implements Todo {
  constructor(
    public id: string,
    public title: string,
    public description: string,
    public completed: boolean = false
  ) {}

  toggle(): void {
    this.completed = !this.completed;
  }

  update(title: string, description: string): void {
    this.title = title;
    this.description = description;
  }
}

L'entité TodoEntity implémente l'interface Todo avec des méthodes métier comme toggle() pour basculer le statut. C'est indépendant : testable sans mocks. Piège à éviter : ne pas ajouter de logique DB ici, sinon vous violez l'indépendance du domaine.

Étape 2 : Définir les repositories (interfaces)

Les repositories sont des interfaces abstraites pour l'accès aux données, définies dans le domaine. Ils exposent des méthodes comme findById() ou save(), sans implémentation. Cela permet d'injecter n'importe quelle DB (InMemory, Mongo, SQL) sans toucher le cœur.

Interface TodoRepository

src/domain/repositories/TodoRepository.ts
import { Todo } from '../entities/Todo';

export interface TodoRepository {
  save(todo: Todo): Promise<Todo>;
  findById(id: string): Promise<Todo | null>;
  findAll(): Promise<Todo[]>;
  update(id: string, todo: Todo): Promise<Todo>;
  delete(id: string): Promise<void>;
}

Cette interface définit le contrat pour tout repository Todo. Les use cases l'utiliseront sans connaître l'implémentation. Avantage : inversion de dépendance – le domaine ne dépend pas de l'infrastructure.

Étape 3 : Implémenter les use cases

Les use cases orchestrent la logique applicative : ils utilisent entités et repositories. Exemple : CreateTodoUseCase valide les inputs et persiste via repository. Analogie : un chef d'orchestre qui dirige sans jouer d'instrument.

Use case : Créer un Todo

src/application/use-cases/CreateTodoUseCase.ts
import { TodoRepository } from '../../domain/repositories/TodoRepository';
import { TodoEntity } from '../../domain/entities/Todo';
import { v4 as uuidv4 } from 'uuid';

export class CreateTodoUseCase {
  constructor(private todoRepository: TodoRepository) {}

  async execute(title: string, description: string): Promise<string> {
    if (!title || title.length < 3) {
      throw new Error('Titre trop court');
    }

    const todo = new TodoEntity(uuidv4(), title, description);
    const savedTodo = await this.todoRepository.save(todo);
    return savedTodo.id;
  }
}

Ce use case valide les inputs métier, crée l'entité et appelle le repository. Il dépend d'une interface, pas d'une impl spécifique. Installez uuid via npm i uuid @types/uuid si needed. Piège : ne pas gérer les erreurs ici pour éviter la pollution métier.

Use case : Lister les Todos

src/application/use-cases/GetTodosUseCase.ts
import { TodoRepository } from '../../domain/repositories/TodoRepository';

export class GetTodosUseCase {
  constructor(private todoRepository: TodoRepository) {}

  async execute(): Promise<any[]> {
    const todos = await this.todoRepository.findAll();
    return todos.map(todo => ({
      id: todo.id,
      title: todo.title,
      description: todo.description,
      completed: todo.completed
    }));
  }
}

Simple récupération et mapping vers DTO (Data Transfer Object) pour éviter d'exposer l'entité interne. Cela protège le domaine des changements externes.

Étape 4 : Implémenter le repository en mémoire

Dans infrastructure, on implémente le repository pour tests rapides. Plus tard, remplacez par Prisma ou Mongoose sans changer le reste.

Repository InMemory

src/infrastructure/repositories/InMemoryTodoRepository.ts
import { Todo } from '../../domain/entities/Todo';
import { TodoRepository } from '../../domain/repositories/TodoRepository';

export class InMemoryTodoRepository implements TodoRepository {
  private todos: Todo[] = [];

  async save(todo: Todo): Promise<Todo> {
    this.todos.push(todo);
    return todo;
  }

  async findById(id: string): Promise<Todo | null> {
    return this.todos.find(t => t.id === id) || null;
  }

  async findAll(): Promise<Todo[]> {
    return [...this.todos];
  }

  async update(id: string, todo: Todo): Promise<Todo> {
    const index = this.todos.findIndex(t => t.id === id);
    if (index === -1) throw new Error('Todo non trouvé');
    this.todos[index] = todo;
    return todo;
  }

  async delete(id: string): Promise<void> {
    const index = this.todos.findIndex(t => t.id === id);
    if (index > -1) this.todos.splice(index, 1);
  }
}

Implémentation synchrone/asynchrone pour matcher l'interface. Copie défensive avec spread pour findAll(). Parfait pour prototypes ; scalable vers DB réelle.

Étape 5 : Créer les adapters (contrôleurs)

Les adapters traduisent les requêtes HTTP vers use cases. Ils dépendent des use cases, pas l'inverse.

Contrôleur Todo

src/adapters/controllers/TodoController.ts
import { Request, Response } from 'express';
import { CreateTodoUseCase } from '../../application/use-cases/CreateTodoUseCase';
import { GetTodosUseCase } from '../../application/use-cases/GetTodosUseCase';

export class TodoController {
  constructor(
    private createTodoUseCase: CreateTodoUseCase,
    private getTodosUseCase: GetTodosUseCase
  ) {}

  async create(req: Request, res: Response): Promise<void> {
    try {
      const { title, description } = req.body;
      const id = await this.createTodoUseCase.execute(title, description);
      res.status(201).json({ id });
    } catch (error) {
      res.status(400).json({ error: (error as Error).message });
    }
  }

  async getAll(req: Request, res: Response): Promise<void> {
    const todos = await this.getTodosUseCase.execute();
    res.json(todos);
  }
}

Le contrôleur injecte les use cases et gère HTTP (status, JSON). Séparation claire : pas de logique métier ici. Ajoutez d'autres méthodes comme update/delete de même.

Étape 6 : Assembler dans le point d'entrée

Enfin, le framework layer (Express) wire tout : instancie repositories et use cases.

Serveur principal

src/presentation/server.ts
import express from 'express';
import { InMemoryTodoRepository } from '../infrastructure/repositories/InMemoryTodoRepository';
import { CreateTodoUseCase } from '../application/use-cases/CreateTodoUseCase';
import { GetTodosUseCase } from '../application/use-cases/GetTodosUseCase';
import { TodoController } from '../adapters/controllers/TodoController';

const app = express();
app.use(express.json());

const todoRepo = new InMemoryTodoRepository();
const createTodoUseCase = new CreateTodoUseCase(todoRepo);
const getTodosUseCase = new GetTodosUseCase(todoRepo);
const todoController = new TodoController(createTodoUseCase, getTodosUseCase);

app.post('/todos', (req, res) => todoController.create(req, res));
app.get('/todos', (req, res) => todoController.getAll(req, res));

const PORT = 3000;
app.listen(PORT, () => {
  console.log(`Serveur sur http://localhost:${PORT}`);
});

export default app;

Wiring des dépendances par composition. Lancez avec npx ts-node src/presentation/server.ts. Testez POST/GET avec curl ou Postman. Facile à swapper repo pour une DB.

Bonnes pratiques

  • Inversion de dépendance : Toujours injecter les interfaces, jamais les implémentations concrètes dans le cœur.
  • DTOs partout : N'exposez jamais les entités aux couches externes pour éviter les couplages.
  • Tests unitaires : Mockez les repositories pour tester use cases isolément.
  • Container IoC : Utilisez InversifyJS ou Awilix pour scaler le wiring.
  • Évolutivité : Ajoutez des ports/adapters pour nouveaux drivers (queues, caches).

Erreurs courantes à éviter

  • Fuite de framework : Ne pas importer Express dans use cases – brise l'indépendance.
  • Entités anémiques : Ajoutez de la logique métier dans entités, pas juste des getters/setters.
  • Repository trop gros : Limitez à 5-7 méthodes ; décomposez si needed.
  • Pas d'erreurs typées : Utilisez custom Error classes pour une meilleure gestion.

Pour aller plus loin

Comment implémenter Clean Architecture en 2026 (TypeScript) | Learni