Chapitre 8
Chapitre 08 — Architecture Front-End
Chapitre 08 — Architecture Front-End
Chapitre 08 — Architecture Front-End : Cours Complet
1. Pourquoi l'Architecture est Cruciale
1.1 Le problème de la non-architecture
Sans architecture, une application front-end évolue inévitablement vers le Big Ball of Mud (grosse boue) :
- Composants qui mélangent UI, état, logique métier et appels API
- Dépendances circulaires entre modules
- Code dupliqué car personne ne sait où mettre les choses
- Tests impossibles sans mock de tout l'univers
- Onboarding d'un nouveau développeur : 3 semaines
1.2 Les signes d'une mauvaise architecture
- Un changement dans le design UI casse la logique métier
- Un changement de base de données ou d'API impacte tous les composants
- Impossible de tester unitairement la logique métier
- Le bundle fait 5MB pour une app "simple"
- Les imports sont chaotiques (
../../utils/helpers/foo/bar/baz) - Les composants "fat" de 500+ lignes
1.3 Objectifs d'une bonne architecture
✅ Maintenable — Un développeur peut modifier le code sans tout casser
✅ Testable — Chaque couche peut être testée indépendamment
✅ Évolutive — On peut ajouter des fonctionnalités sans réécrire
✅ Performante — L'architecture ne doit pas impacter les perfs
✅ Remplaçable — On peut changer une librairie sans tout réécrire
✅ Compréhensible — Un nouveau développeur comprend la structure rapidement
2. Principes SOLID en Front-End
2.1 Single Responsibility Principle (SRP)
Chaque module/fonction/composant doit avoir une seule raison de changer.
// ❌ Mauvais : ce composant gère UI + logique métier + API + state
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(setUser)
.finally(() => setIsLoading(false));
}, [userId]);
if (isLoading) return <Spinner />;
if (!user) return <Error />;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
<button onClick={() => /* modifier le user */}>Edit</button>
</div>
);
}
// ✅ Bon : séparation des responsabilités
function useUser(userId: string) { // Hook : logique de données
return useQuery({ queryKey: ["user", userId], queryFn: () => fetchUser(userId) });
}
function UserProfile({ userId }: { userId: string }) { // Composant : UI uniquement
const { data: user, isLoading, error } = useUser(userId);
if (isLoading) return <Spinner />;
if (error) return <Error error={error} />;
if (!user) return <Empty />;
return <UserProfileView user={user} />;
}
function UserProfileView({ user }: { user: User }) { // Vue : presentation pure
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
2.2 Open/Closed Principle (OCP)
Les modules doivent être ouverts à l'extension, fermés à la modification.
// ❌ Mauvais : ajouter un type de notification = modifier ce code
function Notification({ type, message }: { type: string; message: string }) {
if (type === "success") return <div className="green">{message}</div>;
if (type === "error") return <div className="red">{message}</div>;
if (type === "warning") return <div className="yellow">{message}</div>;
return <div>{message}</div>;
}
// ✅ Bon : Strategy Pattern
interface NotificationStrategy {
render(message: string): React.ReactNode;
}
const strategies: Record<string, NotificationStrategy> = {
success: { render: (m) => <div className="green">{m}</div> },
error: { render: (m) => <div className="red">{m}</div> },
warning: { render: (m) => <div className="yellow">{m}</div> },
};
function Notification({ type, message }: { type: string; message: string }) {
const strategy = strategies[type] ?? strategies.success;
return strategy.render(message);
}
// Ajouter "info" = ajouter une stratégie, pas modifier Notification
2.3 Liskov Substitution Principle (LSP)
Les sous-types doivent être substituables à leurs types de base.
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean;
}
// Ces composants doivent pouvoir remplacer Button sans casser l'app
function PrimaryButton(props: ButtonProps) { return <button className="primary" {...props} />; }
function SecondaryButton(props: ButtonProps) { return <button className="secondary" {...props} />; }
function DangerButton(props: ButtonProps) { return <button className="danger" {...props} />; }
2.4 Interface Segregation Principle (ISP)
Ne pas imposer à un composant des props qu'il n'utilise pas.
// ❌ Mauvais : interface grosse qui force des props inutiles
interface UserFormProps {
id: string;
name: string;
email: string;
role: string;
createdAt: string;
updatedAt: string;
lastLogin: string;
// ... 20 autres champs
}
// ✅ Bon : interfaces spécifiques
interface UserBasicInfo { name: string; email: string; }
interface UserFormActions { onSave: (data: UserBasicInfo) => void; onCancel: () => void; }
type UserFormProps = UserBasicInfo & UserFormActions;
2.5 Dependency Inversion Principle (DIP)
Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau. Les deux doivent dépendre d'abstractions.
// ❌ Mauvais : dépendance directe à une implémentation concrète
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch("/api/users").then(r => r.json()).then(setUsers);
}, []);
// Si on change d'API (GraphQL, tRPC), on doit modifier ce composant
}
// ✅ Bon : abstraction de la source de données
interface UserRepository {
getUsers(): Promise<User[]>;
}
class ApiUserRepository implements UserRepository {
async getUsers(): Promise<User[]> {
const r = await fetch("/api/users");
return r.json();
}
}
class GraphQLUserRepository implements UserRepository {
async getUsers(): Promise<User[]> {
return gql`{ users { id name email } }`;
}
}
function useUsers(repository: UserRepository) {
return useQuery({ queryKey: ["users"], queryFn: () => repository.getUsers() });
}
// Dans le composant, on ne depend que de l'abstraction UserRepository
function UserList({ repository }: { repository: UserRepository }) {
const { data: users } = useUsers(repository);
// ...
}
3. Clean Architecture
3.1 Les cercles
┌────────────────────────────────────────────┐
│ Frameworks & Drivers │
│ (React, Next.js, axios, etc.) │
│ ┌──────────────────────────────────────┐ │
│ │ Interface Adapters │ │
│ │ (Controllers, Presenters, Gateways) │ │
│ │ ┌────────────────────────────────┐ │ │
│ │ │ Application / Use Cases │ │ │
│ │ │ (Actions, Interactors, etc.) │ │ │
│ │ │ ┌──────────────────────────┐ │ │ │
│ │ │ │ Domain / Entities │ │ │ │
│ │ │ │ (Business Logic, Rules) │ │ │ │
│ │ │ └──────────────────────────┘ │ │ │
│ │ └────────────────────────────────┘ │ │
│ └──────────────────────────────────────┘ │
└────────────────────────────────────────────┘
3.2 Application au front-end React
// ─── Domaine (Entities) ───
interface Todo {
id: string;
title: string;
completed: boolean;
createdAt: Date;
}
// ─── Use Cases ───
class AddTodoUseCase {
constructor(private todoRepository: TodoRepository) {}
async execute(title: string): Promise<Todo> {
if (title.length < 3) throw new Error("Title too short");
return this.todoRepository.save({ title, completed: false });
}
}
class ToggleTodoUseCase {
constructor(private todoRepository: TodoRepository) {}
async execute(id: string): Promise<Todo> {
const todo = await this.todoRepository.findById(id);
if (!todo) throw new Error("Todo not found");
return this.todoRepository.update(id, { completed: !todo.completed });
}
}
// ─── Ports (Abstractions) ───
interface TodoRepository {
findAll(): Promise<Todo[]>;
findById(id: string): Promise<Todo | null>;
save(todo: Omit<Todo, "id" | "createdAt">): Promise<Todo>;
update(id: string, data: Partial<Todo>): Promise<Todo>;
delete(id: string): Promise<void>;
}
// ─── Adapters (Implémentations concrètes) ───
class ApiTodoRepository implements TodoRepository {
async findAll(): Promise<Todo[]> {
const r = await fetch("/api/todos");
return r.json();
}
// ...
}
// ─── React Adapter (Interface utilisateur) ───
function useTodos(repository: TodoRepository) {
const queryClient = useQueryClient();
const query = useQuery({ queryKey: ["todos"], queryFn: () => repository.findAll() });
const addTodo = useMutation({
mutationFn: (title: string) => new AddTodoUseCase(repository).execute(title),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["todos"] }),
});
return { todos: query.data, isLoading: query.isLoading, addTodo };
}
4. Architecture Hexagonale (Ports & Adapters)
L'architecture hexagonale (Alistair Cockburn) met l'accent sur le concept de ports (interfaces) et adapters (implémentations). Le domaine est au centre, complètement isolé des technologies externes.
// ─── Ports ───
// Primary ports : l'application expose ces interfaces
interface TodoService {
getTodos(): Promise<Todo[]>;
createTodo(title: string): Promise<Todo>;
completeTodo(id: string): Promise<Todo>;
}
// Secondary ports : l'application a besoin de ces services
interface TodoPersistence {
findAll(): Promise<Todo[]>;
save(todo: Todo): Promise<Todo>;
}
interface Logger {
info(message: string): void;
error(message: string, error: unknown): void;
}
// ─── Application Core (Domain) ───
class TodoServiceImpl implements TodoService {
constructor(
private persistence: TodoPersistence,
private logger: Logger
) {}
async getTodos(): Promise<Todo[]> {
this.logger.info("Fetching todos");
return this.persistence.findAll();
}
async createTodo(title: string): Promise<Todo> {
if (!title.trim()) throw new Error("Title is required");
const todo = { id: crypto.randomUUID(), title, completed: false, createdAt: new Date() };
return this.persistence.save(todo);
}
async completeTodo(id: string): Promise<Todo> {
const todos = await this.persistence.findAll();
const todo = todos.find(t => t.id === id);
if (!todo) throw new Error("Todo not found");
return this.persistence.save({ ...todo, completed: true });
}
}
// ─── Adapters ───
// Primary adapter : React hook
function useTodoService(service: TodoService) {
return useQuery({ queryKey: ["todos"], queryFn: () => service.getTodos() });
}
// Secondary adapter : API REST
class ApiTodoPersistence implements TodoPersistence {
async findAll(): Promise<Todo[]> { /* ... */ }
async save(todo: Todo): Promise<Todo> { /* ... */ }
}
// Secondary adapter : Console Logger
class ConsoleLogger implements Logger {
info(m: string) { console.info(`[INFO] ${m}`); }
error(m: string, e: unknown) { console.error(`[ERROR] ${m}`, e); }
}
5. Domain-Driven Design (DDD) pour le Front-End
5.1 Bounded Context
Dans une application de e-commerce :
┌─────────────────────────────────────────────┐
│ Bounded Context : Catalogue │
│ Ubiquitous Language : Product, Category, │
│ Price, Stock, Variant, SKU │
├─────────────────────────────────────────────┤
│ Bounded Context : Panier │
│ Ubiquitous Language : Cart, CartItem, │
│ Coupon, Tax, Shipping, Total │
├─────────────────────────────────────────────┤
│ Bounded Context : Commande │
│ Ubiquitous Language : Order, OrderLine, │
│ Invoice, Payment, Shipment, Status │
└─────────────────────────────────────────────┘
5.2 Feature Sliced Design (FSD)
FSD est une méthodologie architecturale qui organise le code par fonctionnalités métier plutôt que par type technique.
src/
app/ — Configuration, provider, router
providers.tsx
router.tsx
styles.css
pages/ — Pages de l'application
home/
catalog/
cart/
checkout/
features/ — Fonctionnalités métier réutilisables
auth/
ui/ — Composants UI de l'auth
api/ — Appels API liés à l'auth
model/ — Types et logique métier
lib/ — Utilitaires
cart/
ui/
api/
model/
lib/
search/
ui/
api/
model/
entities/ — Entités métier (Product, User, Order)
product/
ui/ — Composants liés à Product
api/
model/
user/
ui/
api/
model/
shared/ — Code partagé (UI kit, utils)
ui/ — Atomes (Button, Input, Card)
lib/ — Utilitaires génériques
api/ — Client HTTP
config/ — Configuration globale
├── app/ — Layer applicative (routage, providers)
├── processes/ — Processus métier (optionnel)
├── pages/ — Pages composées de features/entities
├── features/ — Fonctionnalités utilisateur
├── entities/ — Entités métier
├── shared/ — Infrastructure partagée
Règle d'or FSD : une couche ne peut importer que les couches en dessous (jamais au-dessus).
pages→features,entities,sharedfeatures→entities,sharedentities→sharedshared→ rien (ou librairies externes)
6. MVC, MVVM, MVP
6.1 MVC (Model-View-Controller)
┌─────────┐ ┌──────────┐ ┌──────────┐
│ Model │◄───►│ View │◄───►│Controller│
│ (Données│ │ (UI) │ │ (Logique)│
│ métier)│ │ │ │ │
└─────────┘ └──────────┘ └──────────┘
React n'est pas MVC, mais on peut voir les hooks comme des contrôleurs et le JSX comme la vue.
6.2 MVVM (Model-View-ViewModel)
┌─────────┐ ┌──────────┐ ┌──────────┐
│ Model │◄───►│ViewModel │◄───►│ View │
│ (Données│ │ (État + │ │ (UI) │
│ métier)│ │ Logique) │ │ │
└─────────┘ └──────────┘ └──────────┘
Le ViewModel expose l'état réactif que la Vue observe. Zustand + React correspond à ce pattern.
6.3 Comparaison
| Pattern | Séparation | Testabilité | Complexité | React |
|---|---|---|---|---|
| MVC | ⚠️ Partielle | ⚠️ Moyenne | ✅ Faible | Composants + Hooks |
| MVVM | ✅ Bonne | ✅ Haute | ⚠️ Modérée | Zustand + Composants |
| Clean/Hexagonal | ✅ Excellente | ✅ Très haute | ❌ Élevée | Ports & Adapters |
| FSD | ✅ Bonne | ✅ Haute | ⚠️ Modérée | Feature folders |
7. Atomic Design (Brad Frost)
7.1 Les 5 niveaux
┌─────────────────────────────────────────┐
│ PAGES (Pages) │
│ Ex: HomePage, ProductPage, CartPage │
│ ┌───────────────────────────────────┐ │
│ │ TEMPLATES (Templates) │ │
│ │ Ex: PageLayout, AuthLayout │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ ORGANISMS (Organismes) │ │ │
│ │ │ Ex: Header, ProductCard, │ │ │
│ │ │ Footer, Navigation, │ │ │
│ │ │ SearchForm │ │ │
│ │ │ ┌───────────────────────┐ │ │ │
│ │ │ │ MOLECULES │ │ │ │
│ │ │ │ Ex: InputGroup, │ │ │ │
│ │ │ │ Card, Modal, Tabs, │ │ │ │
│ │ │ │ Dropdown, Pagination │ │ │ │
│ │ │ │ ┌─────────────────┐ │ │ │ │
│ │ │ │ │ ATOMES │ │ │ │ │
│ │ │ │ │ Ex: Button, │ │ │ │ │
│ │ │ │ │ Input, Label, │ │ │ │ │
│ │ │ │ │ Icon, Badge, │ │ │ │ │
│ │ │ │ │ Tag, Spinner │ │ │ │ │
│ │ │ │ └─────────────────┘ │ │ │ │
│ │ │ └───────────────────────┘ │ │ │
│ │ └─────────────────────────────┘ │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
7.2 Exemple d'implémentation
// Atome
function Button({ variant, size, children, ...props }: ButtonProps) {
return <button className={`btn btn-${variant} btn-${size}`} {...props}>{children}</button>;
}
// Molécule
function InputGroup({ label, error, children }: InputGroupProps) {
return (
<div className="input-group">
<Label text={label} />
{children}
{error && <ErrorMessage message={error} />}
</div>
);
}
// Organisme
function ProductCard({ product, onAddToCart }: ProductCardProps) {
return (
<Card>
<Card.Image src={product.image} />
<Card.Body>
<Heading level={3}>{product.name}</Heading>
<Text>{product.description}</Text>
<Badge variant={product.inStock ? "success" : "error"}>
{product.inStock ? "En stock" : "Rupture"}
</Badge>
<Price amount={product.price} currency="EUR" />
<Button variant="primary" onClick={() => onAddToCart(product)}>
Ajouter au panier
</Button>
</Card.Body>
</Card>
);
}
8. Gestion des États (Loading, Empty, Error, Success)
8.1 Pattern systématique
type AsyncState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error };
// Composant générique pour gérer tous les états
function AsyncRenderer<T>({ state, onSuccess }: {
state: AsyncState<T>;
onSuccess: (data: T) => React.ReactNode;
}) {
switch (state.status) {
case "idle":
return <InitialMessage />;
case "loading":
return <LoadingSkeleton />;
case "error":
return <ErrorFallback error={state.error} onRetry={() => {/* retry */}} />;
case "success":
if (Array.isArray(state.data) && state.data.length === 0) {
return <EmptyState message="Aucune donnée" />;
}
return <>{onSuccess(state.data)}</>;
}
}
9. Comparaison des Approches de Structure
| Approche | Points forts | Points faibles | Quand l'utiliser |
|---|---|---|---|
| Atomic Design | UI cohérente, réutilisable | Trop rigide pour la logique métier | Design System, bibliothèque de composants |
| Feature Sliced | Aligné métier, scalable | Courbe d'apprentissage | Applications enterprise complexes |
| Clean Architecture | Testable, découplé | Beaucoup de boilerplate | Applications critiques, domain-driven |
| Par dossier technique | Simple, familier | Désorganisé à grande échelle | Petits projets, prototypes |
| Par page | Simple, contextuel | Duplication | Landing pages, sites statiques |
10. Anti-patterns Architecturaux
❌ God Component : un composant qui fait tout (data fetching + UI + state)
❌ Props Drilling : passer des props sur 5+ niveaux sans contexte
❌ Global State partout : tout dans Redux même l'état local d'un input
❌ Pas de séparation métier/technique : logique métier mélangée aux appels API
❌ Dépendances circulaires : A importe B qui importe A
❌ Barrel files monstres : index.ts qui exporte tout le module
❌ Couplage fort au framework : impossible de migrer de React vers Vue sans tout réécrire
11. Composition vs Héritage
React favorise la composition :
// ❌ Héritage (pas idiomatique React)
class AdminPage extends BasePage { /* ... */ }
// ✅ Composition
function AdminPage() {
return (
<Page>
<Page.Header>
<AdminHeader />
</Page.Header>
<Page.Content>
<AdminContent />
</Page.Content>
</Page>
);
}
12. Architecture Micro-Frontends (Préparation)
// Module Federation (Webpack 5)
// micro-frontend-host/webpack.config.js
new ModuleFederationPlugin({
name: "host",
remotes: {
catalogue: "catalogue@http://localhost:3001/remoteEntry.js",
cart: "cart@http://localhost:3002/remoteEntry.js",
checkout: "checkout@http://localhost:3003/remoteEntry.js",
},
});
// Dans le code
const CataloguePage = React.lazy(() => import("catalogue/CataloguePage"));
const CartWidget = React.lazy(() => import("cart/CartWidget"));
function App() {
return (
<Suspense fallback={<BigSpinner />}>
<CataloguePage />
<CartWidget />
</Suspense>
);
}
13. Recommandations Finales
- Commencez simple : Atomic Design + Feature folders
- Séparez toujours : UI / Logique métier / Infrastructure / État
- DDD par bounded context pour organiser les fonctionnalités
- Inversion de dépendance pour les services externes
- Un composant = une responsabilité (max 200 lignes)
- Testez la logique métier, pas les détails d'implémentation React
- L'architecture doit évoluer : ne sur-ingenieriez pas au début
- Documentez les décisions (ADR) pour les choix architecturaux importants