Chapitre 16
Chapitre 16 — Architecture Entreprise
> Concevoir, organiser et maintenir des applications front-end à grande échelle.
Architecture Entreprise — Cours Complet
1. Défis des Applications Front-End à Grande Échelle
1.1 Problématiques
Une application front-end à grande échelle doit relever des défis que les petites apps ignorent :
Défis Clés
├── Taille de la codebase (100K → 1M+ lignes)
│ ├── Temps de build (10s → 5min+)
│ ├── Couverture de tests insuffisante
│ └── Réusinage difficile
├── Équipe (1 → 50+ développeurs)
│ ├── Conflits de merge
│ ├── Standards de code incohérents
│ └── Onboarding lent
├── Scale (1K → 10M+ utilisateurs)
│ ├── Performance dégradée
│ ├── Coûts CDN / bande passante
│ └── Points de défaillance uniques
└── Complexité métier
├── Feature flags, A/B testing
├── Multi-tenancy
├── i18n / l10n
└── Conformité (RGPD, accessibilité)
1.2 Principes fondamentaux
- Modularité : découpage en packages/bundles indépendants
- Scalabilité : l'architecture doit passer de 1 à 100 développeurs sans rupture
- Résilience : un module ne doit pas faire tomber toute l'application
- Observabilité : savoir ce qui se passe en production
- Gouvernance : processus pour maintenir la qualité
2. Monorepo
2.1 Pourquoi un monorepo ?
| Aspect | Multi-repo | Monorepo |
|---|---|---|
| Partage de code | Duplication ou packages npm | Import direct |
| Refactoring cross-package | Impossible sans versioning | Transparent |
| Atomic commits | Non | Oui |
| CI/CD | Complexe (N pipelines) | Simplifié |
| Visibilité | Silotée | Totale |
| Outillage | Hétérogène | Unifié |
2.2 Outils de monorepo
Nx (Recommandé pour grandes équipes)
# Créer un workspace Nx
npx create-nx-workspace@latest monorepo --preset=react
# Structure
monorepo/
├── apps/
│ ├── web/ # App React
│ ├── admin/ # App admin
│ └── api/ # Backend BFF
├── libs/
│ ├── shared/ # Code partagé
│ ├── ui/ # Design System
│ └── utils/ # Utilitaires
├── tools/ # Scripts internes
├── nx.json # Config Nx
├── workspace.json
└── package.json
Fonctionnalités clés :
- Dependency graph : visualisation des relations entre packages
- Computation caching : ne rebuild que ce qui a changé
- Affected commands :
nx test --affected - Distributed task execution : parallélisation multi-machine
- Code generation : générateurs pour créer des libs/apps
# Nx en action
nx graph # Visualiser le graphe de dépendances
nx run-many --target=test # Tester tous les projets
nx affected --target=lint # Linter les projets modifiés
nx build web --prod # Build production avec cache
Turborepo
// turbo.json
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": []
},
"lint": {},
"dev": {
"cache": false
}
}
}
npx create-turbo@latest
# Avantage : simplicité, intégration Vercel
pnpm Workspaces
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
pnpm install # Installe toutes les dépendances
pnpm --filter web add # Ajoute une dépendance à un workspace
pnpm -r run build # Build récursif
Lerna (legacy)
npx lerna init
npx lerna bootstrap # Lie les packages entre eux
npx lerna run test # Exécute dans tous les packages
2.3 Choix de l'outil
| Critère | Nx | Turborepo | pnpm | Lerna |
|---|---|---|---|---|
| Cache | ★★★★★ | ★★★★★ | ★★★☆☆ | ★★☆☆☆ |
| Graphe de dépendances | ★★★★★ | ★★★★☆ | ★★★☆☆ | ★★★☆☆ |
| Parallélisation | ★★★★★ | ★★★★★ | ★★★☆☆ | ★★★☆☆ |
| Génération de code | ★★★★★ | ★☆☆☆☆ | ★☆☆☆☆ | ★☆☆☆☆ |
| Facilité d'adoption | ★★★☆☆ | ★★★★★ | ★★★★★ | ★★★★★ |
| Communauté | Très active | Active | Très active | Maintenance |
3. Gouvernance
3.1 Code Reviews
Processus de code review à l'échelle :
# Code Review Checklist
## Architecture
- [ ] La solution suit-elle l'architecture définie ?
- [ ] Y a-t-il des dépendances circulaires ?
- [ ] Le découpage en composants est-il cohérent ?
## Performance
- [ ] Y a-t-il des re-rendus inutiles ?
- [ ] Les bundles sont-ils optimisés (code splitting) ?
- [ ] Les images/assets sont-ils optimisés ?
## Accessibilité
- [ ] Les attributs ARIA sont-ils corrects ?
- [ ] La navigation clavier fonctionne-t-elle ?
- [ ] Les contrastes sont-ils suffisants ?
## Sécurité
- [ ] Les entrées sont-elles assainies ?
- [ ] Les tokens/API keys sont-ils exposés ?
- [ ] Le Content Security Policy est-il respecté ?
## Tests
- [ ] Les nouvelles fonctionnalités sont-elles testées ?
- [ ] Les tests existants passent-ils ?
- [ ] La couverture est-elle suffisante ?
3.2 RFCs (Request For Comments)
Process formel pour les décisions architecturales majeures :
1. Discovery ──► 2. RFC Draft ──► 3. Review ──► 4. Decision ──► 5. Implementation
(problem) (solution) (feedback) (approve/reject) (code)
Template RFC :
# RFC: [Titre]
## Meta
- **Auteur**: Nom
- **Date**: JJ/MM/AAAA
- **Status**: Draft | Review | Accepted | Rejected | Implemented
## Résumé
[2-3 phrases expliquant la proposition]
## Motivation
[Pourquoi ce changement est nécessaire]
## Design détaillé
[Description technique de l'implémentation]
## Alternatives envisagées
[Liste des alternatives et pourquoi elles ont été rejetées]
## Impact
- Breaking changes ?
- Performance ?
- Accessibilité ?
- Sécurité ?
## Plan d'implémentation
[Étapes, milestones]
3.3 ADRs (Architecture Decision Records)
Les ADR sont des documents légers qui capturent chaque décision architecturale importante.
Voir adr/README.md pour un exemple complet.
Structure d'un ADR :
# ADR-NNN: Titre de la décision
## Statut
[Proposed | Accepted | Deprecated | Superseded]
## Contexte
[Pourquoi cette décision est nécessaire]
## Décision
[Ce que nous avons décidé]
## Conséquences
[Ce que cette décision implique (positives et négatives)]
4. Feature Flags
4.1 Pourquoi les feature flags ?
- Déploiement progressif (canary releases)
- A/B testing
- Kill switch (désactiver une fonctionnalité problématique)
- Activation par segment (bêta testeurs, région, plan tarifaire)
- Tests en production sans impact
4.2 Architecture
Application
│
▼
Feature Flag Client
│
├──► LaunchDarkly / Flagsmith (SaaS)
│ │
│ ├── Targeting rules
│ ├── Segment-based
│ ├── A/B testing
│ └── Analytics
│
└──► Custom Implementation (self-hosted)
│
├── Config file (JSON/YAML)
├── API backend
├── Redis cache
└── Admin UI
4.3 Implémentation avec LaunchDarkly
// launchdarkly-client.ts
import { initialize } from 'launchdarkly-js-client-sdk';
const LD_CLIENT_ID = import.meta.env.VITE_LD_CLIENT_ID;
let client: any = null;
export async function initLD(user: { key: string; email: string }) {
client = initialize(LD_CLIENT_ID, user);
await client.waitForInitialization();
}
export function getFlag(key: string, defaultValue = false): boolean {
return client?.variation(key, defaultValue) ?? defaultValue;
}
// HOC pour les feature flags
export function withFeatureFlag(flagKey: string, fallback: React.ComponentType) {
return function WrappedComponent(Component: React.ComponentType) {
return function FeatureFlagWrapper(props: any) {
const enabled = getFlag(flagKey);
if (!enabled) {
return fallback ? React.createElement(fallback, props) : null;
}
return React.createElement(Component, props);
};
};
}
4.4 Implémentation Custom (self-hosted)
// feature-flags.ts
type FlagDefinition = {
key: string;
description: string;
enabled: boolean;
rules?: FlagRule[];
metadata?: Record<string, unknown>;
};
type FlagRule = {
attribute: 'user.email' | 'user.plan' | 'random';
operator: 'equals' | 'contains' | 'in' | 'percentage';
value: string | string[] | number;
};
class FeatureFlagService {
private flags: Map<string, FlagDefinition> = new Map();
private listeners: Set<(key: string, value: boolean) => void> = new Set();
async load(url: string) {
const response = await fetch(url);
const data: FlagDefinition[] = await response.json();
data.forEach(f => this.flags.set(f.key, f));
}
isEnabled(key: string, user?: { email?: string; plan?: string }): boolean {
const flag = this.flags.get(key);
if (!flag) return false;
if (!flag.enabled) return false;
if (flag.rules && user) {
for (const rule of flag.rules) {
if (this.evaluateRule(rule, user)) return true;
}
}
return flag.enabled;
}
private evaluateRule(rule: FlagRule, user: { email?: string; plan?: string }): boolean {
switch (rule.operator) {
case 'equals':
return user[rule.attribute as keyof typeof user] === rule.value;
case 'percentage':
return Math.random() * 100 < (rule.value as number);
case 'in':
return (rule.value as string[]).includes(
user[rule.attribute as keyof typeof user] as string
);
default:
return false;
}
}
}
export const featureFlags = new FeatureFlagService();
// feature-flags.json
[
{
"key": "new-dashboard",
"description": "Nouveau dashboard avec graphiques D3",
"enabled": true,
"rules": [
{ "attribute": "user.plan", "operator": "in", "value": ["premium", "enterprise"] },
{ "attribute": "random", "operator": "percentage", "value": 10 }
]
},
{
"key": "ai-assistant",
"description": "Assistant IA (bêta)",
"enabled": true,
"rules": [
{ "attribute": "user.email", "operator": "contains", "value": "@beta.com" }
]
},
{
"key": "legacy-payments",
"description": "Ancien système de paiement (kill switch)",
"enabled": false
}
]
5. Internationalisation (i18n)
5.1 Architecture i18n
src/
├── i18n/
│ ├── index.ts # Configuration
│ ├── locales/
│ │ ├── en/
│ │ │ ├── common.json
│ │ │ └── dashboard.json
│ │ ├── fr/
│ │ │ ├── common.json
│ │ │ └── dashboard.json
│ │ └── ... (20+ langues)
│ ├── ICU/
│ │ └── messages.ts # Messages complexes
│ └── detector.ts # Détection de la langue
5.2 react-i18next
// i18n/index.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
i18n
.use(Backend) // Charge les fichiers JSON depuis /locales
.use(LanguageDetector) // Détecte la langue du navigateur
.use(initReactI18next) // Intègre React
.init({
fallbackLng: 'en',
debug: process.env.NODE_ENV === 'development',
interpolation: {
escapeValue: false, // React gère déjà l'échappement XSS
},
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
ns: ['common', 'dashboard', 'settings'],
defaultNS: 'common',
});
export default i18n;
5.3 ICU Messages
Les messages ICU gèrent les pluriels, le genre, les dates et les nombres de façon native :
{
"welcome": "Bienvenue, {name} !",
"items": "{count, plural, =0{Aucun article} one{# article} other{# articles}}",
"date": "Publié le {date, date, long}",
"price": "Prix : {value, number, EUR}",
"gender": "{gender, select, male{Il a} female{Elle a} other{Ils ont}} accepté",
"nested": "Bonjour {user.name}, vous avez {user.count, plural, one{# message} other{# messages}}"
}
// Utilisation avec react-i18next
import { useTranslation, Trans } from 'react-i18next';
function Dashboard() {
const { t, i18n } = useTranslation('dashboard');
return (
<div>
<h1>{t('welcome', { name: 'Alice' })}</h1>
<p>{t('items', { count: 3 })}</p>
{/* Composant Trans pour JSX dans les traductions */}
<Trans i18nKey="description" ns="common">
Veuillez <a href="/terms">accepter les conditions</a>.
</Trans>
<button onClick={() => i18n.changeLanguage('fr')}>FR</button>
</div>
);
}
5.4 Bonnes pratiques i18n
- Clés descriptives :
dashboard.welcome.titleplutôt quetitle1 - Namespaces : séparer par module (common, dashboard, settings)
- CI check : vérifier que toutes les clés sont traduites dans toutes les langues
- Lazy loading : charger les traductions à la demande
- Pseudolocalization : tester l'UI avec des chaînes allongées pour détecter les overflow
// Script CI : vérification des traductions
// check-translations.ts
import fs from 'fs';
import path from 'path';
const locales = ['en', 'fr', 'de', 'es', 'ja'];
let hasErrors = false;
for (const locale of locales) {
const filePath = path.join('public', 'locales', locale, 'common.json');
const translations = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
// Vérifier les valeurs vides
for (const [key, value] of Object.entries(translations)) {
if (!value) {
console.error(`❌ ${locale}/${key} est vide`);
hasErrors = true;
}
}
}
if (hasErrors) process.exit(1);
6. Monitoring et Observabilité
6.1 Les 3 piliers de l'observabilité
Observabilité
├── Logs ──── Événements discrets
│ ├── console.log → Log aggregation (Datadog, ELK)
│ └── Log levels : debug, info, warn, error
├── Metrics ── Mesures agrégées
│ ├── Web Vitals (LCP, FID, CLS)
│ ├── Error rates
│ ├── Request latency
│ └── Business metrics (conversion, signups)
└── Traces ── Parcours d'une requête
├── Front-end → API → Database
└── Distributed tracing (OpenTelemetry)
6.2 Implémentation
// monitoring.ts
import * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing';
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
integrations: [new BrowserTracing()],
tracesSampleRate: 0.2, // 20% des sessions
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
environment: import.meta.env.MODE,
});
// Métriques custom
export function trackMetric(name: string, value: number, tags?: Record<string, string>) {
if (window.performance?.measure) {
performance.measure(name);
}
// Envoyer à votre backend de métriques
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/metrics', JSON.stringify({ name, value, tags }));
}
}
// Web Vitals
import { onLCP, onFID, onCLS, onINP, onTTFB } from 'web-vitals';
function reportWebVitals(metric: any) {
trackMetric(metric.name, metric.value);
// Envoyer à Sentry
Sentry.metrics.distribution(metric.name, metric.value);
}
onLCP(reportWebVitals);
onFID(reportWebVitals);
onCLS(reportWebVitals);
onINP(reportWebVitals);
onTTFB(reportWebVitals);
6.3 Dashboard monitoring idéal
// Application Performance Monitoring (APM)
interface FrontendMetrics {
webVitals: {
lcp: number; // Largest Contentful Paint
fid: number; // First Input Delay
cls: number; // Cumulative Layout Shift
inp: number; // Interaction to Next Paint
ttfb: number; // Time to First Byte
};
errors: {
jsErrors: number;
apiErrors: number;
unhandledRejections: number;
};
performance: {
jsHeapSize: number;
domNodes: number;
eventListeners: number;
};
business: {
pageViews: number;
signups: number;
conversions: number;
};
}
7. Sécurité
7.1 Content Security Policy (CSP)
<!-- index.html -->
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self' 'strict-dynamic' https://cdn.sentry.io;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https://*.cloudfront.net;
connect-src 'self' https://api.example.com wss://ws.example.com;
font-src 'self' https://fonts.gstatic.com;
frame-ancestors 'none';
form-action 'self';
base-uri 'self';
report-uri /csp-report;
">
7.2 Protection XSS
// Échappement automatique avec React (JSX)
// ❌ Dangereux
div.innerHTML = userInput;
// ✅ Sécurisé (React échappe automatiquement)
<div>{userInput}</div>;
// Si dangerouslySetInnerHTML est inévitable :
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />;
7.3 Protection CSRF
// API client avec protection CSRF
const api = {
async request<T>(url: string, options: RequestInit = {}): Promise<T> {
const token = getCookie('csrf-token');
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': token,
...options.headers,
},
credentials: 'include', // Envoie les cookies
});
if (response.status === 403) {
// CSRF attack detected
Sentry.captureMessage('CSRF attack detected', 'fatal');
throw new Error('CSRF validation failed');
}
return response.json();
}
};
7.4 OWASP Top 10 pour le front-end
| Rang | Risque | Mitigation Front-End |
|---|---|---|
| 1 | Broken Access Control | RBAC, vérification des permissions côté client |
| 2 | Cryptographic Failures | Ne pas stocker de secrets dans le bundle |
| 3 | Injection (XSS) | React/DOMPurify, CSP, input sanitization |
| 4 | Insecure Design | Security reviews, threat modeling |
| 5 | Security Misconfiguration | CSP, CORS, secure headers |
| 6 | Vulnerable Components | Dependabot, npm audit, Snyk |
| 7 | Auth Failures | JWT secure, httpOnly cookies, OAuth2 PKCE |
| 8 | Data Integrity Failures | Subresource Integrity (SRI) |
| 9 | Logging Failures | Ne pas logger les données sensibles |
| 10 | SSRF | Validation des URLs côté client |
<!-- Subresource Integrity (SRI) -->
<script
src="https://cdn.example.com/app.js"
integrity="sha384-xyzabc123..."
crossorigin="anonymous"
></script>
8. Scale : Des Milliers aux Millions d'Utilisateurs
8.1 Architecture de scale
Utilisateurs (10M+)
│
▼
CDN (Cloudflare, CloudFront, Fastly)
├── Static assets (JS, CSS, images)
└── Edge caching (HTML fragments)
│
▼
Edge Workers (Cloudflare Workers, Lambda@Edge)
├── A/B testing
├── Geolocalisation
├── Personalization
└── API response caching
│
▼
Load Balancer
│
├── Web Servers (auto-scaling)
│ ├── SSR (Next.js, Remix)
│ └── API Gateway
│
└── Services
├── Auth Service
├── Search Service
├── Payment Service
└── Notification Service
8.2 Stratégies de cache
// Service Worker avec stratégies avancées
self.addEventListener('fetch', (event) => {
const { request } = event;
// Stratégie : Cache First pour les assets statiques
if (request.url.match(/\.(js|css|png|jpg)$/)) {
event.respondWith(cacheFirst(request));
}
// Stratégie : Network First pour l'API
if (request.url.includes('/api/')) {
event.respondWith(networkFirst(request));
}
// Stratégie : Stale While Revalidate pour les pages
if (request.mode === 'navigate') {
event.respondWith(staleWhileRevalidate(request));
}
});
async function cacheFirst(request: Request): Promise<Response> {
const cache = await caches.open('static-v1');
const cached = await cache.match(request);
if (cached) return cached;
const response = await fetch(request);
cache.put(request, response.clone());
return response;
}
8.3 Edge Computing
// Cloudflare Worker pour A/B testing
export default {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
const cookie = request.headers.get('Cookie') || '';
// 50% des utilisateurs voient la nouvelle version
const variant = Math.random() < 0.5 ? 'A' : 'B';
// Réécrire l'URL ou injecter un cookie
const response = await fetch(url.toString());
return new Response(response.body, {
status: response.status,
headers: {
...response.headers,
'Set-Cookie': `variant=${variant}; Path=/; Max-Age=3600`,
},
});
}
};
9. Accessibilité à l'Échelle
9.1 Audits automatisés en CI
# .github/workflows/a11y.yml
name: Accessibility Audit
on: [pull_request]
jobs:
a11y:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Run axe-core
run: npx axe http://localhost:3000 --save report.json
- name: Run pa11y
run: npx pa11y-ci --sitemap http://localhost:3000/sitemap.xml
- name: Check WCAG compliance
run: node scripts/check-a11y-report.mjs
- name: Comment on PR
if: failure()
uses: actions/github-script@v7
with:
script: |
const violations = require('./report.json');
// Post comment with violations
9.2 axe-core programmatique
// test/a11y.test.ts
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
describe('Accessibility', () => {
it('Dashboard should have no violations', async () => {
const { container } = render(<Dashboard />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('Form should be accessible', async () => {
const { container } = render(
<form aria-label="Contact form">
<label htmlFor="name">Name</label>
<input id="name" />
<button type="submit">Submit</button>
</form>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
9.3 pa11y-ci configuration
{
"urls": [
"http://localhost:3000/",
"http://localhost:3000/dashboard",
"http://localhost:3000/settings",
"http://localhost:3000/help"
],
"defaults": {
"standard": "WCAG2AA",
"runners": ["axe", "htmlcs"],
"hideElements": ".cookie-banner",
"timeout": 30000,
"viewport": {
"width": 1280,
"height": 1024
}
}
}
10. Data Fetching Patterns
10.1 TanStack React Query (anciennement React Query)
// hooks/useProjects.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
interface Project {
id: string;
name: string;
status: 'active' | 'archived';
}
// Query
export function useProjects() {
return useQuery<Project[]>({
queryKey: ['projects'],
queryFn: () => fetch('/api/projects').then(r => r.json()),
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 30 * 60 * 1000, // Cache GC après 30 min
refetchOnWindowFocus: true,
});
}
// Mutation avec optimistic update
export function useUpdateProject() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (project: Project) =>
fetch(`/api/projects/${project.id}`, {
method: 'PUT',
body: JSON.stringify(project),
}).then(r => r.json()),
onMutate: async (newProject) => {
await queryClient.cancelQueries({ queryKey: ['projects'] });
const previous = queryClient.getQueryData<Project[]>(['projects']);
queryClient.setQueryData<Project[]>(['projects'], (old) =>
old?.map(p => p.id === newProject.id ? newProject : p)
);
return { previous };
},
onError: (err, newProject, context) => {
queryClient.setQueryData(['projects'], context?.previous);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['projects'] });
},
});
}
10.2 GraphQL (Apollo Client)
// graphql/projects.graphql
query GetProjects($status: ProjectStatus) {
projects(status: $status) {
id
name
status
members {
id
name
avatar
}
tasks {
total
completed
}
}
}
mutation UpdateProject($id: ID!, $input: ProjectInput!) {
updateProject(id: $id, input: $input) {
id
name
status
}
}
// hooks/useProjectsGraphQL.ts
import { useQuery, useMutation } from '@apollo/client';
import { GET_PROJECTS, UPDATE_PROJECT } from '../graphql/projects';
export function useProjects(status?: string) {
return useQuery(GET_PROJECTS, {
variables: { status },
fetchPolicy: 'cache-and-network',
});
}
export function useUpdateProject() {
return useMutation(UPDATE_PROJECT, {
update(cache, { data }) {
// Mise à jour optimiste du cache Apollo
cache.modify({
fields: {
projects(existing = []) {
return existing.map((p: any) =>
p.id === data.updateProject.id ? data.updateProject : p
);
},
},
});
},
});
}
10.3 tRPC
// server/router.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
export const appRouter = t.router({
projects: t.procedure
.input(z.object({ status: z.string().optional() }))
.query(async ({ input }) => {
return db.projects.findMany({ where: { status: input.status } });
}),
updateProject: t.procedure
.input(z.object({
id: z.string(),
name: z.string().optional(),
status: z.enum(['active', 'archived']).optional(),
}))
.mutation(async ({ input }) => {
return db.projects.update({
where: { id: input.id },
data: input,
});
}),
});
export type AppRouter = typeof appRouter;
// client/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/router';
export const trpc = createTRPCReact<AppRouter>();
// Utilisation dans un composant
function ProjectList() {
const projects = trpc.projects.useQuery({ status: 'active' });
const updateProject = trpc.updateProject.useMutation({
onSuccess: () => projects.refetch(),
});
if (projects.isLoading) return <Spinner />;
return projects.data?.map(project => (
<ProjectCard
key={project.id}
project={project}
onArchive={() => updateProject.mutate({ id: project.id, status: 'archived' })}
/>
));
}
10.4 Normalisation des données
// lib/normalize.ts
interface NormalizedEntities<T> {
byId: Record<string, T>;
allIds: string[];
}
function normalize<T extends { id: string }>(items: T[]): NormalizedEntities<T> {
return {
byId: Object.fromEntries(items.map(item => [item.id, item])),
allIds: items.map(item => item.id),
};
}
function denormalize<T>({ byId, allIds }: NormalizedEntities<T>): T[] {
return allIds.map(id => byId[id]).filter(Boolean);
}
// Usage : mise à jour O(1)
function updateProject(
state: NormalizedEntities<Project>,
updated: Project
): NormalizedEntities<Project> {
return {
...state,
byId: { ...state.byId, [updated.id]: updated },
};
}
11. State Management à Grande Échelle
11.1 Architecture en couches
Presentation Layer (Components)
│ Props / Context
Application Layer (Hooks / Services)
│ State management
Domain Layer (State / Store)
│ Business logic
Infrastructure Layer (API / Cache)
11.2 Choix du state management
| Solution | Cas d'usage | Taille équipe | Complexité |
|---|---|---|---|
| React Context | État global simple | 1-5 | Faible |
| Zustand | Apps modulaires | 3-20 | Faible |
| Redux Toolkit | Apps complexes | 10-50 | Moyenne |
| Jotai | Apps atomiques | 5-30 | Faible |
| XState | Workflows complexes | 5-20 | Élevée |
| Recoil | Apps Facebook-like | 10-50 | Moyenne |
// Zustand pour un store scale
import { create } from 'zustand';
import { devtools, persist, subscribeWithSelector } from 'zustand/middleware';
interface AppState {
// Auth
user: User | null;
setUser: (user: User | null) => void;
// UI
theme: 'light' | 'dark';
toggleTheme: () => void;
sidebarOpen: boolean;
setSidebarOpen: (open: boolean) => void;
// Projects (état métier)
projects: Project[];
setProjects: (projects: Project[]) => void;
// Feature flags
flags: Record<string, boolean>;
setFlag: (key: string, value: boolean) => void;
}
export const useAppStore = create<AppState>()(
devtools(
persist(
subscribeWithSelector((set) => ({
// Auth
user: null,
setUser: (user) => set({ user }),
// UI
theme: 'light',
toggleTheme: () => set((s) => ({
theme: s.theme === 'light' ? 'dark' : 'light'
})),
sidebarOpen: true,
setSidebarOpen: (open) => set({ sidebarOpen: open }),
// Projects
projects: [],
setProjects: (projects) => set({ projects }),
// Flags
flags: {},
setFlag: (key, value) => set((s) => ({
flags: { ...s.flags, [key]: value }
})),
})),
{
name: 'app-storage',
partialize: (state) => ({ theme: state.theme }),
}
),
{ name: 'AppStore' }
)
);
12. Documentation Front-End
12.1 Storybook
// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: [
'../src/**/*.stories.@(ts|tsx)',
'../packages/**/*.stories.@(ts|tsx)',
],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-a11y',
'@storybook/addon-designs',
'storybook-dark-mode',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
docs: {
autodocs: 'tag',
},
};
export default config;
// src/components/Button/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'UI/Button',
component: Button,
parameters: {
layout: 'centered',
designs: {
type: 'figma',
url: 'https://www.figma.com/file/...',
},
},
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'ghost', 'danger'],
},
size: { control: 'select', options: ['sm', 'md', 'lg'] },
disabled: { control: 'boolean' },
},
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: {
variant: 'primary',
size: 'md',
children: 'Click me',
},
};
export const Disabled: Story = {
args: {
...Primary.args,
disabled: true,
},
};
12.2 Diagrammes d'architecture (Mermaid)
Diagramme en cours de génération...
13. Conclusion
L'architecture entreprise n'est pas une question de technologies spécifiques mais de processus, de gouvernance et de patterns éprouvés. Les décisions prises aujourd'hui impactent la capacité de l'équipe à livrer dans 6 mois, 1 an, 5 ans.
Les clés du succès :
- Standardisation sans rigidité (monorepo, outils communs)
- Gouvernance légère mais efficace (RFCs, ADRs, code reviews)
- Observabilité dès le premier jour (logs, métriques, traces)
- Sécurité intégrée (shift left, audits automatisés)
- Scale anticipé (CDN, edge, caching, lazy loading)
- Accessibilité non-négociable (audits CI, axe-core)
- Documentation vivante (Storybook, ADRs, diagrammes)