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
- Dependance vers l'interieur — les couches exterieures dependent des interieures
- L'inversion de dependance — les interfaces sont definies dans le domaine
- Les use cases sont purs — pas de framework, pas de DB, juste de la logique
- 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.