Ilyes Bouzayen
11 juil. 2026

Clean Architecture en 2026 : Le Guide pour Structurer Votre Code

14 min de lecture

Clean Architecture en 2026 : Le Guide pour Structurer Votre Code

La Clean Architecture n'est pas du sur-design. C'est la différence entre un code qui vit 3 mois et un code qui vit 3 ans.

Les 4 couches

+-------------------------------------------+
|         Frameworks & Drivers              |  <- Externe (Express, Prisma, React)
+-------------------------------------------+
|         Interface Adapters                |  <- Controleurs, Gateways, Presenters
+-------------------------------------------+
|           Use Cases                       |  <- Logique metier pure
+-------------------------------------------+
|         Entities (Domain)                 |  <- Modeles de donnees purs
+-------------------------------------------+

Regle : les dependances vont TOUJOURS vers l'interieur.

Structure de dossiers

src/
  domain/              <- Couche la plus interne
    entities/
      User.ts
    repositories/
      UserRepository.ts    (interface)
  application/         <- Use cases
    usecases/
      CreateUser.ts
      GetUser.ts
  infrastructure/      <- Couche la plus externe
    database/
      PrismaUserRepository.ts
    api/
      ExpressUserController.ts
    email/
      SmtpEmailService.ts
  presentation/        <- UI
    svelte/
      UserForm.svelte

Entites pures (pas de framework)

// domain/entities/User.ts
export class User {
  constructor(
    public readonly id: string,
    public readonly email: string,
    public name: string,
    public role: UserRole,
    public readonly createdAt: Date
  ) {}

  isAdmin(): boolean {
    return this.role === 'admin';
  }

  changeName(newName: string): void {
    if (newName.length < 2) throw new Error('Name too short');
    this.name = newName;
  }
}

export type UserRole = 'admin' | 'user' | 'guest';

Repository (interface)

// domain/repositories/UserRepository.ts
export interface UserRepository {
  findById(id: string): Promise<User | null>;
  findByEmail(email: string): Promise<User | null>;
  save(user: User): Promise<void>;
  delete(id: string): Promise<void>;
}

// domain/repositories/EmailService.ts
export interface EmailService {
  send(to: string, subject: string, body: string): Promise<void>;
}

Implementation concrete

// infrastructure/database/PrismaUserRepository.ts
import { UserRepository } from '../../domain/repositories/UserRepository';
import { User, UserRole } from '../../domain/entities/User';

export class PrismaUserRepository implements UserRepository {
  constructor(private prisma: PrismaClient) {}

  async findById(id: string): Promise<User | null> {
    const data = await this.prisma.user.findUnique({ where: { id } });
    if (!data) return null;
    return new User(data.id, data.email, data.name, data.role as UserRole, data.createdAt);
  }

  async findByEmail(email: string): Promise<User | null> {
    const data = await this.prisma.user.findUnique({ where: { email } });
    if (!data) return null;
    return new User(data.id, data.email, data.name, data.role as UserRole, data.createdAt);
  }

  async save(user: User): Promise<void> {
    await this.prisma.user.upsert({
      where: { id: user.id },
      create: { id: user.id, email: user.email, name: user.name, role: user.role },
      update: { name: user.name, role: user.role }
    });
  }

  async delete(id: string): Promise<void> {
    await this.prisma.user.delete({ where: { id } });
  }
}

Use Case (la logique metier)

// application/usecases/CreateUser.ts
import { User } from '../../domain/entities/User';
import { UserRepository } from '../../domain/repositories/UserRepository';
import { EmailService } from '../../domain/repositories/EmailService';
import { randomUUID } from 'crypto';

export class CreateUser {
  constructor(
    private userRepo: UserRepository,
    private emailService: EmailService
  ) {}

  async execute(input: { email: string; name: string }): Promise<User> {
    // Verifier l'unicite
    const existing = await this.userRepo.findByEmail(input.email);
    if (existing) throw new Error('Email already taken');

    // Creer l'entite
    const user = new User(
      randomUUID(),
      input.email,
      input.name,
      'user',
      new Date()
    );

    // Persister
    await this.userRepo.save(user);

    // Notifier
    await this.emailService.send(
      user.email,
      'Bienvenue !',
      `Bonjour ${user.name}, bienvenue sur la plateforme.`
    );

    return user;
  }
}

Testabilite

// Test du use case SANS Prisma, SANS email, SANS Express
describe('CreateUser', () => {
  it('should create a user and send email', async () => {
    const mockRepo = {
      findByEmail: vi.fn().mockResolvedValue(null),
      save: vi.fn().mockResolvedValue(undefined)
    };
    const mockEmail = {
      send: vi.fn().mockResolvedValue(undefined)
    };

    const useCase = new CreateUser(mockRepo, mockEmail);
    const user = await useCase.execute({
      email: 'test@example.com',
      name: 'Ilyes'
    });

    expect(user.email).toBe('test@example.com');
    expect(mockRepo.save).toHaveBeenCalledOnce();
    expect(mockEmail.send).toHaveBeenCalledOnce();
  });

  it('should reject duplicate email', async () => {
    const mockRepo = {
      findByEmail: vi.fn().mockResolvedValue({ id: '1', email: 'test@example.com' })
    };

    const useCase = new CreateUser(mockRepo, { send: vi.fn() });
    await expect(
      useCase.execute({ email: 'test@example.com', name: 'Ilyes' })
    ).rejects.toThrow('Email already taken');
  });
});

Les principes cles

  1. Dependance vers l'interieur — les couches exterieures dependent des interieures
  2. L'inversion de dependance — les interfaces sont definies dans le domaine
  3. Les use cases sont purs — pas de framework, pas de DB, juste de la logique
  4. Les entites ne changent pas — si Prisma est remplace, le domaine reste identique

Conclusion

Clean Architecture = un code qui survit aux changements de frameworks, de BDD, et d'equipes.