{"id":50372524,"url":"https://github.com/gasycoder/cours-framework-js-isstm-l3","last_synced_at":"2026-05-30T08:01:49.578Z","repository":{"id":338629575,"uuid":"1158519953","full_name":"GasyCoder/cours-framework-js-isstm-l3","owner":"GasyCoder","description":"Auteur : Support de cours universitaire - BEZARA Florent Niveau : Licence 3 — Informatique Prérequis : Bases solides en HTML, CSS, JavaScript (ES6+), notions de programmation orientée objet Volume horaire indicatif : 40 à 50 heures (cours + TP)","archived":false,"fork":false,"pushed_at":"2026-03-23T04:55:17.000Z","size":120,"stargazers_count":8,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-03-24T01:00:19.136Z","etag":null,"topics":["angular","reactjs","vuejs"],"latest_commit_sha":null,"homepage":"https://me.gasycoder.com/","language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/GasyCoder.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-15T14:09:56.000Z","updated_at":"2026-03-23T04:55:21.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/GasyCoder/cours-framework-js-isstm-l3","commit_stats":null,"previous_names":["gasycoder/cours-framework-js-isstm-l3"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/GasyCoder/cours-framework-js-isstm-l3","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GasyCoder%2Fcours-framework-js-isstm-l3","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GasyCoder%2Fcours-framework-js-isstm-l3/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GasyCoder%2Fcours-framework-js-isstm-l3/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GasyCoder%2Fcours-framework-js-isstm-l3/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/GasyCoder","download_url":"https://codeload.github.com/GasyCoder/cours-framework-js-isstm-l3/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GasyCoder%2Fcours-framework-js-isstm-l3/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33684413,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-30T02:00:06.278Z","response_time":92,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["angular","reactjs","vuejs"],"created_at":"2026-05-30T08:01:48.495Z","updated_at":"2026-05-30T08:01:49.561Z","avatar_url":"https://github.com/GasyCoder.png","language":"HTML","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Cours complet : Les Frameworks JavaScript\n\n### Du Frontend au Backend\n---\n\n## Table des matières\n\n1. [Introduction générale](#1-introduction-générale)\n2. [Rappels JavaScript essentiels (ES6+)](#2-rappels-javascript-essentiels-es6)\n3. [Qu'est-ce qu'un framework JavaScript ?](#3-quest-ce-quun-framework-javascript)\n4. [Partie I — Le Frontend](#partie-i--le-frontend)\n   - [Chapitre 4 : Les concepts fondamentaux du frontend moderne](#chapitre-4--les-concepts-fondamentaux-du-frontend-moderne)\n   - [Chapitre 5 : React.js — La bibliothèque de composants](#chapitre-5--reactjs--la-bibliothèque-de-composants)\n   - [Chapitre 6 : Vue.js — Le framework progressif](./cours/vuejs-fondamentaux.md) ([TP Installation](./tp/tp-vuejs-installation.md))\n   - [Chapitre 7 : Angular — Le framework complet](#chapitre-7--angular--le-framework-complet)\n   - [Chapitre 8 : Comparaison et choix d'un framework frontend](#chapitre-8--comparaison-et-choix-dun-framework-frontend)\n5. [Partie II — Le Backend](#partie-ii--le-backend)\n   - [Chapitre 9 : Node.js — JavaScript côté serveur](#chapitre-9--nodejs--javascript-côté-serveur)\n   - [Chapitre 10 : Express.js — Le framework backend de référence](#chapitre-10--expressjs--le-framework-backend-de-référence)\n   - [Chapitre 11 : API RESTful — Conception et implémentation](#chapitre-11--api-restful--conception-et-implémentation)\n   - [Chapitre 12 : Connexion à une base de données](#chapitre-12--connexion-à-une-base-de-données)\n   - [Chapitre 13 : Authentification et sécurité](#chapitre-13--authentification-et-sécurité)\n   - [Chapitre 14 : Les frameworks backend avancés](#chapitre-14--les-frameworks-backend-avancés)\n6. [Partie III — Fullstack et écosystème](#partie-iii--fullstack-et-écosystème)\n   - [Chapitre 15 : Next.js et Nuxt.js — Le rendu côté serveur](#chapitre-15--nextjs-et-nuxtjs--le-rendu-côté-serveur)\n   - [Chapitre 16 : Projet intégrateur fullstack](#chapitre-16--projet-intégrateur-fullstack)\n7. [Conclusion générale et perspectives](#conclusion-générale-et-perspectives)\n8. [Bibliographie et ressources](#bibliographie-et-ressources)\n\n---\n\n## 1. Introduction générale\n\n### 1.1 Contexte et motivation\n\nLe développement web a profondément évolué depuis les premières pages HTML statiques des années 1990. Aujourd'hui, les applications web sont devenues aussi complexes et riches que les applications de bureau traditionnelles. Des services comme Gmail, Trello, Spotify ou Netflix sont des applications web à part entière, offrant des interfaces interactives et réactives.\n\nCette évolution a été rendue possible grâce à un langage de programmation : **JavaScript**. Initialement conçu en 1995 par Brendan Eich pour ajouter de l'interactivité aux pages web dans le navigateur Netscape, JavaScript est devenu le langage le plus utilisé au monde. Sa particularité unique est de fonctionner à la fois **côté client** (dans le navigateur) et **côté serveur** (grâce à Node.js), ce qui permet de construire des applications complètes avec un seul langage.\n\nCependant, écrire des applications complexes en JavaScript pur (dit « vanilla ») devient rapidement ingérable : le code devient difficile à organiser, à maintenir et à faire évoluer. C'est précisément pour répondre à ce problème que les **frameworks** et **bibliothèques** JavaScript ont été créés.\n\n### 1.2 Objectifs du cours\n\nÀ l'issue de ce cours, vous serez capable de :\n\n- Comprendre ce qu'est un framework JavaScript et pourquoi il est nécessaire dans le développement moderne.\n- Maîtriser les concepts fondamentaux du développement frontend avec les frameworks React, Vue et Angular.\n- Développer des API côté serveur avec Node.js et Express.js.\n- Connecter une application à une base de données.\n- Mettre en œuvre des mécanismes d'authentification et de sécurité.\n- Réaliser un projet fullstack complet en JavaScript.\n\n### 1.3 Organisation du cours\n\nLe cours est organisé en trois grandes parties qui suivent une progression logique :\n\n- **Partie I — Frontend** : Vous apprendrez à construire des interfaces utilisateur modernes, interactives et performantes.\n- **Partie II — Backend** : Vous découvrirez comment créer des serveurs, des API et gérer les données côté serveur.\n- **Partie III — Fullstack** : Vous assemblerez les deux parties pour construire une application complète.\n\n---\n\n## 2. Rappels JavaScript essentiels (ES6+)\n\nAvant d'aborder les frameworks, il est indispensable de maîtriser les fonctionnalités modernes de JavaScript introduites depuis ECMAScript 2015 (ES6). Ces fonctionnalités sont massivement utilisées dans tous les frameworks.\n\n### 2.1 Déclarations de variables : `let` et `const`\n\nAvant ES6, on utilisait uniquement `var` pour déclarer des variables. Désormais, on préfère `let` et `const` qui offrent une portée de bloc (block scope) plus prévisible.\n\n```javascript\n// var a une portée de fonction (function scope) — à éviter\nvar nom = \"Alice\";\n\n// let a une portée de bloc — utilisable quand la valeur change\nlet age = 25;\nage = 26; // OK : réassignation autorisée\n\n// const a une portée de bloc — la référence ne peut pas être réassignée\nconst PI = 3.14159;\n// PI = 3.14; // ERREUR : réassignation impossible\n\n// Attention : const ne rend pas l'objet immuable\nconst etudiant = { nom: \"Bob\", age: 22 };\netudiant.age = 23; // OK : on modifie une propriété, pas la référence\n```\n\n**Règle pratique :** Utilisez toujours `const` par défaut. N'utilisez `let` que lorsque vous avez réellement besoin de réassigner la variable. N'utilisez jamais `var` dans du code moderne.\n\n### 2.2 Fonctions fléchées (Arrow Functions)\n\nLes fonctions fléchées offrent une syntaxe plus concise et un comportement spécifique du mot-clé `this` (il conserve le `this` du contexte englobant).\n\n```javascript\n// Fonction classique\nfunction addition(a, b) {\n  return a + b;\n}\n\n// Fonction fléchée — forme complète\nconst addition = (a, b) =\u003e {\n  return a + b;\n};\n\n// Fonction fléchée — forme raccourcie (retour implicite)\nconst addition = (a, b) =\u003e a + b;\n\n// Avec un seul paramètre, les parenthèses sont optionnelles\nconst doubler = x =\u003e x * 2;\n\n// Utilisation courante avec les méthodes de tableau\nconst nombres = [1, 2, 3, 4, 5];\nconst pairs = nombres.filter(n =\u003e n % 2 === 0);       // [2, 4]\nconst doubles = nombres.map(n =\u003e n * 2);               // [2, 4, 6, 8, 10]\nconst somme = nombres.reduce((acc, n) =\u003e acc + n, 0);  // 15\n```\n\n### 2.3 Déstructuration (Destructuring)\n\nLa déstructuration permet d'extraire des valeurs d'objets ou de tableaux de manière élégante.\n\n```javascript\n// Déstructuration d'objet\nconst etudiant = {\n  nom: \"Alice\",\n  age: 22,\n  filiere: \"Informatique\",\n  universite: \"Université de Mahajanga\"\n};\n\n// Au lieu de : const nom = etudiant.nom; const age = etudiant.age;\nconst { nom, age, filiere } = etudiant;\nconsole.log(nom);     // \"Alice\"\nconsole.log(filiere); // \"Informatique\"\n\n// Renommage lors de la déstructuration\nconst { nom: nomEtudiant, age: ageEtudiant } = etudiant;\n\n// Valeur par défaut\nconst { note = 0 } = etudiant; // note vaudra 0 car elle n'existe pas\n\n// Déstructuration de tableau\nconst couleurs = [\"rouge\", \"vert\", \"bleu\"];\nconst [premiere, deuxieme, troisieme] = couleurs;\nconsole.log(premiere); // \"rouge\"\n\n// Ignorer des éléments\nconst [, , derniere] = couleurs;\nconsole.log(derniere); // \"bleu\"\n```\n\n### 2.4 Opérateur spread (`...`) et rest\n\nL'opérateur `...` (trois points) sert à deux choses selon le contexte.\n\n```javascript\n// SPREAD : « étaler » les éléments d'un tableau ou d'un objet\n\n// Copie superficielle d'un tableau\nconst original = [1, 2, 3];\nconst copie = [...original];           // [1, 2, 3]\nconst etendu = [...original, 4, 5];    // [1, 2, 3, 4, 5]\n\n// Fusion de tableaux\nconst a = [1, 2];\nconst b = [3, 4];\nconst fusion = [...a, ...b]; // [1, 2, 3, 4]\n\n// Copie et modification d'objets (très utilisé dans React et Vue)\nconst config = { theme: \"dark\", langue: \"fr\" };\nconst nouvelleConfig = { ...config, langue: \"en\" }; \n// { theme: \"dark\", langue: \"en\" }\n\n// REST : collecter le « reste » des arguments\nfunction journaliser(premier, ...autres) {\n  console.log(\"Premier :\", premier);\n  console.log(\"Autres :\", autres);\n}\njournaliser(\"A\", \"B\", \"C\", \"D\");\n// Premier : A\n// Autres : [\"B\", \"C\", \"D\"]\n```\n\n### 2.5 Template literals (Littéraux de gabarit)\n\n```javascript\nconst nom = \"Alice\";\nconst age = 22;\n\n// Avant ES6\nconst message = \"Bonjour, je suis \" + nom + \" et j'ai \" + age + \" ans.\";\n\n// Avec les template literals (backticks `)\nconst message = `Bonjour, je suis ${nom} et j'ai ${age} ans.`;\n\n// Multiligne\nconst html = `\n  \u003cdiv class=\"carte\"\u003e\n    \u003ch2\u003e${nom}\u003c/h2\u003e\n    \u003cp\u003eÂge : ${age}\u003c/p\u003e\n  \u003c/div\u003e\n`;\n\n// Expressions dans les interpolations\nconst prix = 1500;\nconst tva = 0.2;\nconsole.log(`Prix TTC : ${prix * (1 + tva)} Ar`); // \"Prix TTC : 1800 Ar\"\n```\n\n### 2.6 Modules (import / export)\n\nLes modules permettent de découper le code en fichiers distincts et réutilisables. C'est le fondement de l'architecture de tout framework.\n\n```javascript\n// ===== fichier : mathUtils.js =====\n\n// Export nommé (on peut en avoir plusieurs par fichier)\nexport const PI = 3.14159;\n\nexport function carre(x) {\n  return x * x;\n}\n\nexport function cube(x) {\n  return x * x * x;\n}\n\n// Export par défaut (un seul par fichier)\nexport default function addition(a, b) {\n  return a + b;\n}\n\n// ===== fichier : main.js =====\n\n// Import de l'export par défaut\nimport addition from './mathUtils.js';\n\n// Import des exports nommés\nimport { PI, carre, cube } from './mathUtils.js';\n\n// Import combiné\nimport addition, { PI, carre } from './mathUtils.js';\n\n// Import avec alias\nimport { carre as carreDeNombre } from './mathUtils.js';\n\nconsole.log(addition(3, 4));  // 7\nconsole.log(carre(5));        // 25\nconsole.log(PI);              // 3.14159\n```\n\n### 2.7 Promesses et async/await\n\nJavaScript est **asynchrone par nature**. Les Promesses et `async/await` permettent de gérer les opérations qui prennent du temps (appels réseau, lecture de fichiers) sans bloquer l'exécution du programme.\n\n```javascript\n// Une Promesse représente une valeur qui sera disponible dans le futur\n\n// Création d'une Promesse\nconst maPromesse = new Promise((resolve, reject) =\u003e {\n  const succes = true;\n  \n  setTimeout(() =\u003e {\n    if (succes) {\n      resolve(\"Opération réussie !\");\n    } else {\n      reject(\"Erreur : opération échouée.\");\n    }\n  }, 2000); // Simule un délai de 2 secondes\n});\n\n// Consommation avec .then() et .catch()\nmaPromesse\n  .then(resultat =\u003e console.log(resultat))   // \"Opération réussie !\"\n  .catch(erreur =\u003e console.error(erreur));\n\n// Syntaxe async/await — plus lisible (syntaxe recommandée)\nasync function recupererDonnees() {\n  try {\n    const reponse = await fetch('https://api.exemple.com/etudiants');\n    \n    if (!reponse.ok) {\n      throw new Error(`Erreur HTTP : ${reponse.status}`);\n    }\n    \n    const donnees = await reponse.json();\n    console.log(donnees);\n    return donnees;\n  } catch (erreur) {\n    console.error(\"Erreur lors de la récupération :\", erreur.message);\n  }\n}\n\n// Appel de la fonction asynchrone\nrecupererDonnees();\n```\n\n**Point clé :** `await` ne peut être utilisé que dans une fonction déclarée avec `async`. Il « met en pause » l'exécution de la fonction jusqu'à ce que la Promesse soit résolue, mais il ne bloque pas le reste du programme.\n\n### 2.8 Méthodes de tableau fonctionnelles\n\nCes méthodes sont omniprésentes dans les frameworks pour manipuler et afficher des données.\n\n```javascript\nconst etudiants = [\n  { nom: \"Alice\", note: 16, filiere: \"Info\" },\n  { nom: \"Bob\", note: 12, filiere: \"Math\" },\n  { nom: \"Charlie\", note: 18, filiere: \"Info\" },\n  { nom: \"Diana\", note: 9, filiere: \"Info\" },\n  { nom: \"Eve\", note: 14, filiere: \"Math\" },\n];\n\n// map() — Transforme chaque élément et retourne un nouveau tableau\nconst noms = etudiants.map(e =\u003e e.nom);\n// [\"Alice\", \"Bob\", \"Charlie\", \"Diana\", \"Eve\"]\n\n// filter() — Garde uniquement les éléments qui satisfont la condition\nconst bonsEtudiants = etudiants.filter(e =\u003e e.note \u003e= 14);\n// [{ nom: \"Alice\", ... }, { nom: \"Charlie\", ... }, { nom: \"Eve\", ... }]\n\n// find() — Retourne le premier élément qui satisfait la condition\nconst charlie = etudiants.find(e =\u003e e.nom === \"Charlie\");\n// { nom: \"Charlie\", note: 18, filiere: \"Info\" }\n\n// reduce() — Réduit le tableau à une seule valeur\nconst moyenneGenerale = etudiants.reduce((acc, e) =\u003e acc + e.note, 0) / etudiants.length;\n// 13.8\n\n// some() — Vérifie si AU MOINS UN élément satisfait la condition\nconst aDesEchecs = etudiants.some(e =\u003e e.note \u003c 10); // true\n\n// every() — Vérifie si TOUS les éléments satisfont la condition\nconst tousReussis = etudiants.every(e =\u003e e.note \u003e= 10); // false\n\n// Chaînage de méthodes — très puissant\nconst nomsInfoReussis = etudiants\n  .filter(e =\u003e e.filiere === \"Info\")\n  .filter(e =\u003e e.note \u003e= 10)\n  .map(e =\u003e e.nom)\n  .sort();\n// [\"Alice\", \"Charlie\"]\n```\n\n---\n\n## 3. Qu'est-ce qu'un framework JavaScript ?\n\n### 3.1 Définition\n\nUn **framework** (cadriciel en français) est un ensemble structuré de code, de conventions et d'outils qui fournit un cadre de travail pour développer des applications. Il impose une architecture et des bonnes pratiques, permettant au développeur de se concentrer sur la logique métier plutôt que sur la plomberie technique.\n\nLa distinction entre **framework** et **bibliothèque** (library) est importante :\n\n- **Bibliothèque** : Vous appelez le code de la bibliothèque quand vous en avez besoin. Vous gardez le contrôle du flux d'exécution. Exemple : jQuery, Lodash, React (techniquement).\n- **Framework** : Le framework appelle votre code. C'est lui qui contrôle le flux d'exécution et vous fournit des points d'extension. Exemple : Angular, Vue.js, Express.js.\n\nCe principe s'appelle l'**inversion de contrôle** (Inversion of Control — IoC). Dans un framework, c'est le framework qui orchestre l'application ; votre code s'insère dans les emplacements prévus à cet effet.\n\n\u003e **Note :** React est techniquement une bibliothèque (il gère uniquement la vue), mais il est souvent considéré comme un framework car son écosystème (React Router, Redux, Next.js) couvre tous les aspects du développement.\n\n### 3.2 Pourquoi utiliser un framework ?\n\nImaginez que vous devez construire une maison. Vous pourriez fabriquer chaque brique, chaque clou et chaque outil vous-même, ou vous pourriez utiliser des matériaux et outils standards. Un framework, c'est l'équivalent de ces matériaux et outils standards dans le développement web.\n\nLes avantages concrets sont les suivants :\n\n- **Productivité accrue** : Le framework gère les tâches répétitives (routage, gestion d'état, manipulation du DOM) pour que vous puissiez vous concentrer sur votre application.\n- **Maintenabilité** : L'architecture imposée permet à n'importe quel développeur connaissant le framework de comprendre rapidement le code.\n- **Performance** : Les frameworks modernes utilisent des techniques sophistiquées (DOM virtuel, compilation, lazy loading) pour optimiser les performances.\n- **Écosystème** : Chaque framework dispose de milliers de bibliothèques complémentaires, de documentation abondante et d'une communauté active.\n- **Sécurité** : Les frameworks intègrent des protections contre les vulnérabilités courantes (XSS, CSRF, injection).\n\n### 3.3 Panorama de l'écosystème JavaScript\n\nVoici une vue d'ensemble des principaux frameworks et bibliothèques, classés par domaine.\n\n**Frontend (interface utilisateur) :**\n\n| Outil | Type | Créateur | Première version | Philosophie |\n|-------|------|----------|-----------------|-------------|\n| React | Bibliothèque | Meta (Facebook) | 2013 | Composants déclaratifs, DOM virtuel |\n| Vue.js | Framework progressif | Evan You | 2014 | Simplicité, adoption incrémentale |\n| Angular | Framework complet | Google | 2016 (v2+) | Plateforme complète, TypeScript natif |\n| Svelte | Compilateur | Rich Harris | 2016 | Compilation, pas de runtime |\n\n**Backend (côté serveur) :**\n\n| Outil | Type | Fondement | Philosophie |\n|-------|------|-----------|-------------|\n| Node.js | Environnement d'exécution | Moteur V8 de Chrome | JavaScript côté serveur |\n| Express.js | Framework minimaliste | Node.js | Simplicité, flexibilité |\n| NestJS | Framework structuré | Node.js + TypeScript | Architecture Angular-like |\n| Fastify | Framework performant | Node.js | Vitesse, schéma JSON |\n\n**Fullstack (frontend + backend intégrés) :**\n\n| Outil | Basé sur | Particularité |\n|-------|----------|---------------|\n| Next.js | React | SSR, SSG, API Routes |\n| Nuxt.js | Vue.js | SSR, SSG, auto-import |\n\n### 3.4 Le concept de SPA (Single Page Application)\n\nLa plupart des frameworks frontend modernes construisent des **SPA** (Applications à Page Unique). Contrairement aux sites web traditionnels où chaque lien déclenche le chargement complet d'une nouvelle page depuis le serveur, une SPA charge une seule page HTML initiale, puis met à jour dynamiquement le contenu grâce à JavaScript.\n\n**Fonctionnement d'un site traditionnel (Multi-Page Application) :**\n\n```\nUtilisateur clique → Requête HTTP au serveur → Serveur génère le HTML complet\n→ Navigateur recharge toute la page → Affichage\n```\n\n**Fonctionnement d'une SPA :**\n\n```\nUtilisateur clique → JavaScript intercepte le clic → Requête AJAX/Fetch au serveur\n→ Serveur renvoie uniquement les données (JSON) → JavaScript met à jour la partie\nconcernée de la page → Aucun rechargement complet\n```\n\nLes avantages d'une SPA sont une expérience utilisateur fluide (pas de rechargement), des transitions rapides entre les vues, et une séparation nette entre le frontend et le backend (communication via API). L'inconvénient principal est un premier chargement plus lent (tout le JavaScript doit être téléchargé) et des défis pour le référencement (SEO) — problèmes que les frameworks fullstack comme Next.js résolvent.\n\n---\n\n# Partie I — Le Frontend\n\n---\n\n## Chapitre 4 : Les concepts fondamentaux du frontend moderne\n\nAvant de plonger dans un framework spécifique, comprenons les concepts communs à tous les frameworks frontend modernes. Ces concepts constituent le vocabulaire que vous retrouverez dans React, Vue et Angular.\n\n### 4.1 Les composants\n\nUn **composant** est une unité autonome et réutilisable de l'interface utilisateur. C'est le concept le plus important du développement frontend moderne.\n\nPensez à un composant comme à un bloc LEGO : chaque pièce a une forme, une couleur et un rôle précis. On les assemble pour créer des structures complexes. De la même manière, un composant encapsule son propre HTML (structure), CSS (style) et JavaScript (comportement).\n\n```\nPage d'accueil\n├── Composant Header\n│   ├── Composant Logo\n│   └── Composant Navigation\n│       ├── Composant LienNav (\"Accueil\")\n│       ├── Composant LienNav (\"Cours\")\n│       └── Composant LienNav (\"Contact\")\n├── Composant ContenuPrincipal\n│   ├── Composant CarteEtudiant (Alice)\n│   ├── Composant CarteEtudiant (Bob)\n│   └── Composant CarteEtudiant (Charlie)\n└── Composant Footer\n```\n\nChaque `CarteEtudiant` est le même composant réutilisé trois fois avec des données différentes.\n\n### 4.2 Les props (propriétés)\n\nLes **props** sont des données transmises d'un composant parent à un composant enfant. Elles sont **en lecture seule** : le composant enfant ne doit jamais modifier les props qu'il reçoit.\n\nC'est comparable aux paramètres d'une fonction : le parent appelle le composant enfant en lui passant des données.\n\n```\n[Parent : ListeEtudiants]  ──props { nom, note }──▶  [Enfant : CarteEtudiant]\n```\n\n### 4.3 L'état (state)\n\nL'**état** représente les données internes d'un composant qui peuvent changer au fil du temps. Quand l'état change, le framework met automatiquement à jour l'affichage. C'est ce qu'on appelle la **réactivité**.\n\nLa différence entre props et state est fondamentale :\n\n| Caractéristique | Props | State |\n|-----------------|-------|-------|\n| Origine | Vient du parent | Interne au composant |\n| Modification | Lecture seule | Modifiable |\n| Responsabilité | Le parent contrôle | Le composant lui-même contrôle |\n| Mise à jour | Déclenche un re-rendu si changement | Déclenche un re-rendu si changement |\n\n### 4.4 Le cycle de vie des composants\n\nChaque composant passe par des phases au cours de son existence. Comprendre ce cycle de vie est essentiel pour savoir quand effectuer certaines opérations (charger des données, nettoyer des ressources).\n\n```\nCRÉATION (Mounting)\n  │  Le composant est créé et inséré dans le DOM.\n  │  C'est ici qu'on charge les données initiales (appels API).\n  ▼\nMISE À JOUR (Updating)\n  │  Les props ou le state changent, le composant se re-rend.\n  │  C'est ici qu'on réagit aux changements de données.\n  ▼\nDESTRUCTION (Unmounting)\n  │  Le composant est retiré du DOM.\n  │  C'est ici qu'on nettoie (timers, abonnements, connexions).\n  ▼\nFIN\n```\n\n### 4.5 Le DOM virtuel\n\nLe **DOM** (Document Object Model) est la représentation en mémoire de la structure HTML de la page. Manipuler directement le DOM réel est coûteux en performances car chaque modification déclenche un recalcul du rendu par le navigateur.\n\nLe **DOM virtuel** est une technique utilisée par React et Vue : au lieu de modifier directement le DOM réel, le framework maintient une copie légère du DOM en mémoire (le DOM virtuel). Quand l'état change, le framework crée un nouveau DOM virtuel, le compare avec l'ancien (algorithme de « diffing »), identifie les différences minimales, puis applique uniquement ces changements au DOM réel.\n\n```\nÉtat change → Nouveau DOM virtuel → Comparaison (diff) → Modifications minimales → DOM réel mis à jour\n```\n\n### 4.6 Le data binding (liaison de données)\n\nLe **data binding** établit un lien entre les données JavaScript et l'interface HTML.\n\n- **One-way binding** (liaison unidirectionnelle) : Les données vont dans un seul sens, du code vers la vue. Si une variable change dans le code, la vue se met à jour, mais pas l'inverse. C'est l'approche de React.\n- **Two-way binding** (liaison bidirectionnelle) : Les données vont dans les deux sens. Si l'utilisateur tape dans un champ de texte, la variable JavaScript se met à jour automatiquement, et inversement. C'est l'approche d'Angular et Vue (avec `v-model`).\n\n---\n\n## Chapitre 5 : React.js — La bibliothèque de composants\n\n### 5.1 Présentation\n\nReact a été créé par Jordan Walke chez Facebook en 2013 pour résoudre les problèmes de complexité de l'interface du réseau social. Aujourd'hui, React est la bibliothèque frontend la plus populaire au monde, utilisée par Facebook, Instagram, Netflix, Airbnb, Uber et des millions d'autres projets.\n\nLes principes fondamentaux de React sont au nombre de trois :\n\n1. **Déclaratif** : Vous décrivez *ce que* l'interface doit afficher, et React se charge du *comment*.\n2. **Composants** : L'interface est décomposée en composants réutilisables.\n3. **Unidirectionnel** : Les données circulent du parent vers l'enfant (one-way data flow).\n\n### 5.2 Installation et création d'un projet\n\nPour créer un nouveau projet React, l'outil officiel recommandé est **Vite** (prononcé « vite », c'est un outil français !).\n\n```bash\n# Prérequis : Node.js (version 18+) et npm installés\n# Vérifiez votre installation :\nnode --version   # doit afficher v18.x.x ou supérieur\nnpm --version    # doit afficher 9.x.x ou supérieur\n\n# Créer un nouveau projet React avec Vite\nnpm create vite@latest mon-app-react -- --template react\n\n# Se déplacer dans le dossier du projet\ncd mon-app-react\n\n# Installer les dépendances\nnpm install\n\n# Lancer le serveur de développement\nnpm run dev\n\n# Le terminal affichera une URL comme http://localhost:5173\n# Ouvrez-la dans votre navigateur\n```\n\nStructure du projet créé :\n\n```\nmon-app-react/\n├── node_modules/        # Dépendances installées (ne pas toucher)\n├── public/              # Fichiers statiques (images, favicon)\n├── src/                 # Code source de l'application\n│   ├── App.css          # Styles du composant App\n│   ├── App.jsx          # Composant principal\n│   ├── index.css        # Styles globaux\n│   └── main.jsx         # Point d'entrée de l'application\n├── index.html           # Page HTML unique (SPA)\n├── package.json         # Configuration du projet et dépendances\n└── vite.config.js       # Configuration de Vite\n```\n\n### 5.3 JSX — HTML dans JavaScript\n\n**JSX** (JavaScript XML) est une extension syntaxique qui permet d'écrire du HTML directement dans le code JavaScript. Ce n'est pas du HTML standard — c'est du sucre syntaxique qui est transformé en appels de fonctions JavaScript par le compilateur.\n\n```jsx\n// Ceci est du JSX :\nconst element = \u003ch1 className=\"titre\"\u003eBonjour, Monde !\u003c/h1\u003e;\n\n// Le compilateur le transforme en :\nconst element = React.createElement('h1', { className: 'titre' }, 'Bonjour, Monde !');\n```\n\nRègles importantes du JSX :\n\n```jsx\n// 1. Un seul élément racine (utiliser un Fragment \u003c\u003e si nécessaire)\n// ❌ INCORRECT\nreturn (\n  \u003ch1\u003eTitre\u003c/h1\u003e\n  \u003cp\u003eParagraphe\u003c/p\u003e\n);\n\n// ✅ CORRECT — avec une div englobante\nreturn (\n  \u003cdiv\u003e\n    \u003ch1\u003eTitre\u003c/h1\u003e\n    \u003cp\u003eParagraphe\u003c/p\u003e\n  \u003c/div\u003e\n);\n\n// ✅ CORRECT — avec un Fragment (ne crée pas de nœud DOM supplémentaire)\nreturn (\n  \u003c\u003e\n    \u003ch1\u003eTitre\u003c/h1\u003e\n    \u003cp\u003eParagraphe\u003c/p\u003e\n  \u003c/\u003e\n);\n\n// 2. Les expressions JavaScript s'insèrent avec des accolades { }\nconst nom = \"Alice\";\nreturn \u003ch1\u003eBonjour, {nom} !\u003c/h1\u003e;\n\n// 3. « class » devient « className » (class est un mot réservé en JS)\nreturn \u003cdiv className=\"conteneur\"\u003e...\u003c/div\u003e;\n\n// 4. « for » devient « htmlFor »\nreturn \u003clabel htmlFor=\"email\"\u003eEmail :\u003c/label\u003e;\n\n// 5. Le style inline s'écrit avec un objet JavaScript\nreturn \u003cdiv style={{ color: 'blue', fontSize: '18px', marginTop: '10px' }}\u003e...\u003c/div\u003e;\n\n// 6. Toutes les balises doivent être fermées, y compris les auto-fermantes\nreturn \u003cimg src=\"photo.jpg\" alt=\"Photo\" /\u003e;  // noter le /\n```\n\n### 5.4 Composants fonctionnels\n\nEn React moderne, tous les composants sont des **fonctions** qui retournent du JSX. Les composants basés sur des classes existent mais ne sont plus recommandés pour le nouveau code.\n\n```jsx\n// ===== fichier : src/components/Salutation.jsx =====\n\n// Un composant fonctionnel simple\n// Convention : le nom commence toujours par une MAJUSCULE\nfunction Salutation() {\n  return (\n    \u003cdiv\u003e\n      \u003ch2\u003eBienvenue sur notre plateforme !\u003c/h2\u003e\n      \u003cp\u003eNous sommes ravis de vous accueillir.\u003c/p\u003e\n    \u003c/div\u003e\n  );\n}\n\n// Export pour pouvoir l'utiliser dans d'autres fichiers\nexport default Salutation;\n\n// ===== fichier : src/App.jsx =====\n\nimport Salutation from './components/Salutation';\n\nfunction App() {\n  return (\n    \u003cdiv\u003e\n      \u003ch1\u003eMon Application\u003c/h1\u003e\n      \u003cSalutation /\u003e    {/* Utilisation du composant */}\n      \u003cSalutation /\u003e    {/* Réutilisation — le composant apparaît deux fois */}\n    \u003c/div\u003e\n  );\n}\n\nexport default App;\n```\n\n### 5.5 Props — Passer des données aux composants\n\nLes props permettent de rendre les composants dynamiques et réutilisables.\n\n```jsx\n// ===== fichier : src/components/CarteEtudiant.jsx =====\n\n// Les props sont reçues comme paramètre de la fonction\n// On utilise la déstructuration pour plus de clarté\nfunction CarteEtudiant({ nom, filiere, note, photo }) {\n  // Déterminer le statut selon la note\n  const estAdmis = note \u003e= 10;\n  \n  return (\n    \u003cdiv style={{\n      border: '1px solid #ddd',\n      borderRadius: '8px',\n      padding: '16px',\n      margin: '8px',\n      backgroundColor: estAdmis ? '#e8f5e9' : '#ffebee'\n    }}\u003e\n      {/* Affichage conditionnel de l'image */}\n      {photo \u0026\u0026 \u003cimg src={photo} alt={nom} style={{ width: '80px', borderRadius: '50%' }} /\u003e}\n      \n      \u003ch3\u003e{nom}\u003c/h3\u003e\n      \u003cp\u003eFilière : {filiere}\u003c/p\u003e\n      \u003cp\u003eNote : {note}/20\u003c/p\u003e\n      \u003cp style={{ fontWeight: 'bold', color: estAdmis ? 'green' : 'red' }}\u003e\n        {estAdmis ? '✅ Admis(e)' : '❌ Ajourné(e)'}\n      \u003c/p\u003e\n    \u003c/div\u003e\n  );\n}\n\nexport default CarteEtudiant;\n\n// ===== fichier : src/App.jsx =====\n\nimport CarteEtudiant from './components/CarteEtudiant';\n\nfunction App() {\n  return (\n    \u003cdiv style={{ padding: '20px' }}\u003e\n      \u003ch1\u003eListe des étudiants\u003c/h1\u003e\n      \u003cdiv style={{ display: 'flex', flexWrap: 'wrap' }}\u003e\n        \u003cCarteEtudiant nom=\"Alice Rakoto\" filiere=\"Informatique\" note={16} /\u003e\n        \u003cCarteEtudiant nom=\"Bob Rabe\" filiere=\"Mathématiques\" note={8} /\u003e\n        \u003cCarteEtudiant nom=\"Charlie Andria\" filiere=\"Informatique\" note={14} /\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n  );\n}\n\nexport default App;\n```\n\n### 5.6 State avec le hook `useState`\n\nLe **state** (état local) permet à un composant de gérer des données qui changent au fil du temps. En React, on utilise le hook `useState`.\n\n```jsx\nimport { useState } from 'react';\n\nfunction Compteur() {\n  // useState retourne un tableau de 2 éléments :\n  // [1] La valeur actuelle de l'état\n  // [2] Une fonction pour modifier cet état\n  const [compteur, setCompteur] = useState(0); // 0 est la valeur initiale\n  \n  // IMPORTANT : Ne JAMAIS modifier l'état directement !\n  // ❌ compteur = compteur + 1;     // Ne déclenchera PAS de re-rendu\n  // ✅ setCompteur(compteur + 1);   // Déclenche un re-rendu\n\n  return (\n    \u003cdiv\u003e\n      \u003ch2\u003eCompteur : {compteur}\u003c/h2\u003e\n      \u003cbutton onClick={() =\u003e setCompteur(compteur + 1)}\u003e\n        Incrémenter (+1)\n      \u003c/button\u003e\n      \u003cbutton onClick={() =\u003e setCompteur(compteur - 1)}\u003e\n        Décrémenter (-1)\n      \u003c/button\u003e\n      \u003cbutton onClick={() =\u003e setCompteur(0)}\u003e\n        Réinitialiser\n      \u003c/button\u003e\n    \u003c/div\u003e\n  );\n}\n\nexport default Compteur;\n```\n\nExemple plus complet — un formulaire d'inscription :\n\n```jsx\nimport { useState } from 'react';\n\nfunction FormulaireInscription() {\n  // Chaque champ du formulaire a son propre état\n  const [formData, setFormData] = useState({\n    nom: '',\n    email: '',\n    filiere: 'informatique',\n    accepteConditions: false\n  });\n  \n  const [soumis, setSoumis] = useState(false);\n  const [erreurs, setErreurs] = useState({});\n\n  // Gestionnaire générique pour tous les champs\n  const handleChange = (e) =\u003e {\n    const { name, value, type, checked } = e.target;\n    setFormData(prev =\u003e ({\n      ...prev,                                    // Copie de l'état précédent\n      [name]: type === 'checkbox' ? checked : value  // Mise à jour du champ concerné\n    }));\n  };\n\n  // Validation du formulaire\n  const valider = () =\u003e {\n    const nouvellesErreurs = {};\n    if (!formData.nom.trim()) nouvellesErreurs.nom = \"Le nom est requis.\";\n    if (!formData.email.includes('@')) nouvellesErreurs.email = \"Email invalide.\";\n    if (!formData.accepteConditions) nouvellesErreurs.conditions = \"Vous devez accepter.\";\n    return nouvellesErreurs;\n  };\n\n  const handleSubmit = (e) =\u003e {\n    e.preventDefault(); // Empêche le rechargement de la page\n    const nouvellesErreurs = valider();\n    \n    if (Object.keys(nouvellesErreurs).length === 0) {\n      setSoumis(true);\n      console.log(\"Données soumises :\", formData);\n    } else {\n      setErreurs(nouvellesErreurs);\n    }\n  };\n\n  // Affichage conditionnel après soumission\n  if (soumis) {\n    return (\n      \u003cdiv style={{ padding: '20px', backgroundColor: '#e8f5e9', borderRadius: '8px' }}\u003e\n        \u003ch2\u003eInscription réussie !\u003c/h2\u003e\n        \u003cp\u003eBienvenue, {formData.nom} !\u003c/p\u003e\n        \u003cp\u003eUn email de confirmation a été envoyé à {formData.email}.\u003c/p\u003e\n      \u003c/div\u003e\n    );\n  }\n\n  return (\n    \u003cdiv style={{ maxWidth: '400px', margin: '0 auto', padding: '20px' }}\u003e\n      \u003ch2\u003eInscription\u003c/h2\u003e\n      \n      \u003cdiv style={{ marginBottom: '15px' }}\u003e\n        \u003clabel htmlFor=\"nom\"\u003eNom complet :\u003c/label\u003e\n        \u003cinput\n          type=\"text\"\n          id=\"nom\"\n          name=\"nom\"\n          value={formData.nom}\n          onChange={handleChange}\n          style={{ width: '100%', padding: '8px', marginTop: '4px' }}\n        /\u003e\n        {erreurs.nom \u0026\u0026 \u003cspan style={{ color: 'red', fontSize: '14px' }}\u003e{erreurs.nom}\u003c/span\u003e}\n      \u003c/div\u003e\n\n      \u003cdiv style={{ marginBottom: '15px' }}\u003e\n        \u003clabel htmlFor=\"email\"\u003eEmail :\u003c/label\u003e\n        \u003cinput\n          type=\"email\"\n          id=\"email\"\n          name=\"email\"\n          value={formData.email}\n          onChange={handleChange}\n          style={{ width: '100%', padding: '8px', marginTop: '4px' }}\n        /\u003e\n        {erreurs.email \u0026\u0026 \u003cspan style={{ color: 'red', fontSize: '14px' }}\u003e{erreurs.email}\u003c/span\u003e}\n      \u003c/div\u003e\n\n      \u003cdiv style={{ marginBottom: '15px' }}\u003e\n        \u003clabel htmlFor=\"filiere\"\u003eFilière :\u003c/label\u003e\n        \u003cselect\n          id=\"filiere\"\n          name=\"filiere\"\n          value={formData.filiere}\n          onChange={handleChange}\n          style={{ width: '100%', padding: '8px', marginTop: '4px' }}\n        \u003e\n          \u003coption value=\"informatique\"\u003eInformatique\u003c/option\u003e\n          \u003coption value=\"mathematiques\"\u003eMathématiques\u003c/option\u003e\n          \u003coption value=\"physique\"\u003ePhysique\u003c/option\u003e\n        \u003c/select\u003e\n      \u003c/div\u003e\n\n      \u003cdiv style={{ marginBottom: '15px' }}\u003e\n        \u003clabel\u003e\n          \u003cinput\n            type=\"checkbox\"\n            name=\"accepteConditions\"\n            checked={formData.accepteConditions}\n            onChange={handleChange}\n          /\u003e\n          {' '}J'accepte les conditions d'utilisation\n        \u003c/label\u003e\n        {erreurs.conditions \u0026\u0026 \u003cspan style={{ color: 'red', fontSize: '14px', display: 'block' }}\u003e{erreurs.conditions}\u003c/span\u003e}\n      \u003c/div\u003e\n\n      \u003cbutton\n        onClick={handleSubmit}\n        style={{\n          width: '100%', padding: '10px',\n          backgroundColor: '#1976d2', color: 'white',\n          border: 'none', borderRadius: '4px', cursor: 'pointer'\n        }}\n      \u003e\n        S'inscrire\n      \u003c/button\u003e\n    \u003c/div\u003e\n  );\n}\n\nexport default FormulaireInscription;\n```\n\n### 5.7 Le hook `useEffect` — Effets de bord\n\n`useEffect` permet d'exécuter du code en réaction à des événements du cycle de vie du composant : après le premier rendu, après chaque mise à jour, ou lors du démontage.\n\n```jsx\nimport { useState, useEffect } from 'react';\n\nfunction ListeUtilisateurs() {\n  const [utilisateurs, setUtilisateurs] = useState([]);\n  const [chargement, setChargement] = useState(true);\n  const [erreur, setErreur] = useState(null);\n\n  // useEffect avec un tableau de dépendances vide [] \n  // → s'exécute UNE SEULE FOIS après le premier rendu (équivalent de componentDidMount)\n  useEffect(() =\u003e {\n    async function chargerDonnees() {\n      try {\n        const reponse = await fetch('https://jsonplaceholder.typicode.com/users');\n        \n        if (!reponse.ok) {\n          throw new Error('Erreur réseau');\n        }\n        \n        const donnees = await reponse.json();\n        setUtilisateurs(donnees);\n      } catch (err) {\n        setErreur(err.message);\n      } finally {\n        setChargement(false);\n      }\n    }\n    \n    chargerDonnees();\n  }, []); // ← Tableau de dépendances vide = exécution au montage uniquement\n\n  // Gestion des différents états d'affichage\n  if (chargement) return \u003cp\u003eChargement en cours...\u003c/p\u003e;\n  if (erreur) return \u003cp style={{ color: 'red' }}\u003eErreur : {erreur}\u003c/p\u003e;\n\n  return (\n    \u003cdiv\u003e\n      \u003ch2\u003eUtilisateurs ({utilisateurs.length})\u003c/h2\u003e\n      \u003cul\u003e\n        {utilisateurs.map(user =\u003e (\n          \u003cli key={user.id}\u003e\n            \u003cstrong\u003e{user.name}\u003c/strong\u003e — {user.email}\n          \u003c/li\u003e\n        ))}\n      \u003c/ul\u003e\n    \u003c/div\u003e\n  );\n}\n\nexport default ListeUtilisateurs;\n```\n\nLes trois formes de `useEffect` :\n\n```jsx\n// 1. Sans tableau de dépendances → s'exécute après CHAQUE rendu\nuseEffect(() =\u003e {\n  console.log(\"Rendu effectué\");\n});\n\n// 2. Avec tableau vide → s'exécute UNE FOIS au montage\nuseEffect(() =\u003e {\n  console.log(\"Composant monté\");\n}, []);\n\n// 3. Avec dépendances → s'exécute quand une dépendance change\nuseEffect(() =\u003e {\n  console.log(`Le compteur vaut maintenant : ${compteur}`);\n}, [compteur]); // Se déclenche quand 'compteur' change\n\n// 4. Avec fonction de nettoyage (cleanup)\nuseEffect(() =\u003e {\n  const timer = setInterval(() =\u003e {\n    console.log(\"Tick\");\n  }, 1000);\n  \n  // La fonction retournée est appelée au démontage du composant\n  return () =\u003e {\n    clearInterval(timer); // Nettoyage pour éviter les fuites de mémoire\n  };\n}, []);\n```\n\n### 5.8 Rendu conditionnel et listes\n\n```jsx\nfunction TableauDeBord({ utilisateur, notifications }) {\n  return (\n    \u003cdiv\u003e\n      {/* Condition avec \u0026\u0026 (ET logique) */}\n      {utilisateur \u0026\u0026 \u003ch1\u003eBonjour, {utilisateur.nom} !\u003c/h1\u003e}\n      \n      {/* Condition ternaire */}\n      {utilisateur \n        ? \u003cp\u003eVous êtes connecté.\u003c/p\u003e \n        : \u003cp\u003eVeuillez vous connecter.\u003c/p\u003e\n      }\n      \n      {/* Rendu d'une liste avec map() */}\n      {/* IMPORTANT : chaque élément d'une liste doit avoir une prop \"key\" unique */}\n      \u003ch2\u003eNotifications\u003c/h2\u003e\n      {notifications.length === 0 ? (\n        \u003cp\u003eAucune notification.\u003c/p\u003e\n      ) : (\n        \u003cul\u003e\n          {notifications.map(notif =\u003e (\n            \u003cli key={notif.id} style={{\n              color: notif.lue ? 'gray' : 'black',\n              fontWeight: notif.lue ? 'normal' : 'bold'\n            }}\u003e\n              {notif.message} — {new Date(notif.date).toLocaleDateString('fr-FR')}\n            \u003c/li\u003e\n          ))}\n        \u003c/ul\u003e\n      )}\n    \u003c/div\u003e\n  );\n}\n```\n\n### 5.9 Communication enfant vers parent\n\nLes données descendent via les props, mais comment un enfant communique-t-il avec son parent ? En passant une **fonction callback** en prop.\n\n```jsx\nimport { useState } from 'react';\n\n// Composant enfant\nfunction FiltreFiliere({ filiereActive, onChangerFiliere }) {\n  const filieres = ['Toutes', 'Informatique', 'Mathématiques', 'Physique'];\n  \n  return (\n    \u003cdiv style={{ marginBottom: '20px' }}\u003e\n      {filieres.map(filiere =\u003e (\n        \u003cbutton\n          key={filiere}\n          onClick={() =\u003e onChangerFiliere(filiere)}  // Appelle la fonction du parent\n          style={{\n            padding: '8px 16px',\n            margin: '0 4px',\n            backgroundColor: filiereActive === filiere ? '#1976d2' : '#e0e0e0',\n            color: filiereActive === filiere ? 'white' : 'black',\n            border: 'none',\n            borderRadius: '4px',\n            cursor: 'pointer'\n          }}\n        \u003e\n          {filiere}\n        \u003c/button\u003e\n      ))}\n    \u003c/div\u003e\n  );\n}\n\n// Composant parent\nfunction PageEtudiants() {\n  const [filiere, setFiliere] = useState('Toutes');\n  \n  const etudiants = [\n    { id: 1, nom: \"Alice\", filiere: \"Informatique\", note: 16 },\n    { id: 2, nom: \"Bob\", filiere: \"Mathématiques\", note: 12 },\n    { id: 3, nom: \"Charlie\", filiere: \"Informatique\", note: 14 },\n    { id: 4, nom: \"Diana\", filiere: \"Physique\", note: 11 },\n  ];\n  \n  const etudiantsFiltres = filiere === 'Toutes' \n    ? etudiants \n    : etudiants.filter(e =\u003e e.filiere === filiere);\n\n  return (\n    \u003cdiv\u003e\n      \u003ch1\u003eGestion des étudiants\u003c/h1\u003e\n      {/* On passe la fonction setFiliere en prop */}\n      \u003cFiltreFiliere filiereActive={filiere} onChangerFiliere={setFiliere} /\u003e\n      \n      \u003cp\u003e{etudiantsFiltres.length} étudiant(s) affiché(s)\u003c/p\u003e\n      \u003cul\u003e\n        {etudiantsFiltres.map(e =\u003e (\n          \u003cli key={e.id}\u003e{e.nom} — {e.filiere} — {e.note}/20\u003c/li\u003e\n        ))}\n      \u003c/ul\u003e\n    \u003c/div\u003e\n  );\n}\n\nexport default PageEtudiants;\n```\n\n### 5.10 React Router — Navigation dans une SPA\n\nReact Router est la bibliothèque standard pour gérer la navigation entre les différentes « pages » d'une application React.\n\n```bash\n# Installation\nnpm install react-router-dom\n```\n\n```jsx\n// ===== fichier : src/App.jsx =====\n\nimport { BrowserRouter, Routes, Route, Link, NavLink } from 'react-router-dom';\n\n// Pages (composants)\nfunction Accueil() {\n  return \u003cdiv\u003e\u003ch2\u003ePage d'accueil\u003c/h2\u003e\u003cp\u003eBienvenue sur notre application !\u003c/p\u003e\u003c/div\u003e;\n}\n\nfunction Etudiants() {\n  return \u003cdiv\u003e\u003ch2\u003eListe des étudiants\u003c/h2\u003e\u003cp\u003eIci s'affichera la liste.\u003c/p\u003e\u003c/div\u003e;\n}\n\nfunction APropos() {\n  return \u003cdiv\u003e\u003ch2\u003eÀ propos\u003c/h2\u003e\u003cp\u003eApplication de gestion universitaire.\u003c/p\u003e\u003c/div\u003e;\n}\n\nfunction PageNonTrouvee() {\n  return \u003cdiv\u003e\u003ch2\u003e404 — Page non trouvée\u003c/h2\u003e\u003cp\u003eCette page n'existe pas.\u003c/p\u003e\u003c/div\u003e;\n}\n\n// Barre de navigation\nfunction BarreNavigation() {\n  const styleActif = { fontWeight: 'bold', color: '#1976d2' };\n  \n  return (\n    \u003cnav style={{ padding: '10px', backgroundColor: '#f5f5f5', marginBottom: '20px' }}\u003e\n      {/* NavLink ajoute automatiquement une classe \"active\" au lien courant */}\n      \u003cNavLink to=\"/\" style={({ isActive }) =\u003e isActive ? styleActif : {}}\u003e\n        Accueil\n      \u003c/NavLink\u003e\n      {' | '}\n      \u003cNavLink to=\"/etudiants\" style={({ isActive }) =\u003e isActive ? styleActif : {}}\u003e\n        Étudiants\n      \u003c/NavLink\u003e\n      {' | '}\n      \u003cNavLink to=\"/a-propos\" style={({ isActive }) =\u003e isActive ? styleActif : {}}\u003e\n        À propos\n      \u003c/NavLink\u003e\n    \u003c/nav\u003e\n  );\n}\n\n// Application principale avec le routeur\nfunction App() {\n  return (\n    \u003cBrowserRouter\u003e\n      \u003cBarreNavigation /\u003e\n      \u003cdiv style={{ padding: '20px' }}\u003e\n        \u003cRoutes\u003e\n          \u003cRoute path=\"/\" element={\u003cAccueil /\u003e} /\u003e\n          \u003cRoute path=\"/etudiants\" element={\u003cEtudiants /\u003e} /\u003e\n          \u003cRoute path=\"/a-propos\" element={\u003cAPropos /\u003e} /\u003e\n          \u003cRoute path=\"*\" element={\u003cPageNonTrouvee /\u003e} /\u003e  {/* Route catch-all */}\n        \u003c/Routes\u003e\n      \u003c/div\u003e\n    \u003c/BrowserRouter\u003e\n  );\n}\n\nexport default App;\n```\n\n---\n\n## Chapitre 6 : Vue.js — Le framework progressif\n\n### 6.1 Présentation\n\nVue.js a été créé en 2014 par Evan You, un ancien ingénieur de Google qui a voulu prendre les meilleures idées d'Angular et de React pour créer un framework plus simple et accessible. Vue se distingue par sa philosophie **progressive** : vous pouvez l'adopter progressivement, en commençant par l'utiliser pour une seule partie d'une page existante, puis l'étendre à toute l'application.\n\nLes caractéristiques principales de Vue sont sa courbe d'apprentissage douce, son excellent système de réactivité, sa syntaxe intuitive avec les directives, et une documentation considérée comme l'une des meilleures de l'écosystème.\n\n### 6.2 Installation et création d'un projet\n\n```bash\n# Créer un nouveau projet Vue avec l'outil officiel create-vue (basé sur Vite)\nnpm create vue@latest mon-app-vue\n\n# L'outil vous posera des questions interactives :\n# ✔ Add TypeScript? → No (pour commencer)\n# ✔ Add JSX Support? → No\n# ✔ Add Vue Router? → Yes\n# ✔ Add Pinia for state management? → Yes\n# ✔ Add ESLint? → Yes\n\ncd mon-app-vue\nnpm install\nnpm run dev\n```\n\nStructure du projet :\n\n```\nmon-app-vue/\n├── src/\n│   ├── assets/          # Ressources statiques (images, CSS)\n│   ├── components/      # Composants réutilisables\n│   ├── router/          # Configuration du routeur\n│   ├── stores/          # Stores Pinia (gestion d'état)\n│   ├── views/           # Composants « pages »\n│   ├── App.vue          # Composant racine\n│   └── main.js          # Point d'entrée\n├── index.html\n├── package.json\n└── vite.config.js\n```\n\n### 6.3 Les Single File Components (SFC)\n\nEn Vue, chaque composant est défini dans un fichier `.vue` qui contient trois sections : le template (HTML), le script (JavaScript) et le style (CSS).\n\n```vue\n\u003c!-- ===== fichier : src/components/CarteEtudiant.vue ===== --\u003e\n\n\u003ctemplate\u003e\n  \u003c!-- Section HTML — le template du composant --\u003e\n  \u003cdiv class=\"carte\" :class=\"{ admis: estAdmis, ajourne: !estAdmis }\"\u003e\n    \u003ch3\u003e{{ nom }}\u003c/h3\u003e\n    \u003cp\u003eFilière : {{ filiere }}\u003c/p\u003e\n    \u003cp\u003eNote : {{ note }}/20\u003c/p\u003e\n    \u003cp class=\"statut\"\u003e\n      {{ estAdmis ? '✅ Admis(e)' : '❌ Ajourné(e)' }}\n    \u003c/p\u003e\n  \u003c/div\u003e\n\u003c/template\u003e\n\n\u003cscript setup\u003e\n// Section JavaScript — la logique du composant\n// \u003cscript setup\u003e est la syntaxe moderne recommandée (Composition API)\n\nimport { computed } from 'vue'\n\n// Déclaration des props que ce composant accepte\nconst props = defineProps({\n  nom: {\n    type: String,\n    required: true\n  },\n  filiere: {\n    type: String,\n    required: true\n  },\n  note: {\n    type: Number,\n    required: true,\n    validator(value) {\n      return value \u003e= 0 \u0026\u0026 value \u003c= 20\n    }\n  }\n})\n\n// Propriété calculée (computed) — se recalcule automatiquement quand ses dépendances changent\nconst estAdmis = computed(() =\u003e props.note \u003e= 10)\n\u003c/script\u003e\n\n\u003cstyle scoped\u003e\n/* Section CSS — les styles du composant */\n/* \"scoped\" signifie que ces styles ne s'appliquent qu'à CE composant */\n\n.carte {\n  border: 1px solid #ddd;\n  border-radius: 8px;\n  padding: 16px;\n  margin: 8px;\n  transition: transform 0.2s;\n}\n\n.carte:hover {\n  transform: translateY(-2px);\n  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);\n}\n\n.admis {\n  background-color: #e8f5e9;\n  border-color: #4caf50;\n}\n\n.ajourne {\n  background-color: #ffebee;\n  border-color: #f44336;\n}\n\n.statut {\n  font-weight: bold;\n  font-size: 1.1em;\n}\n\u003c/style\u003e\n```\n\n### 6.4 Les directives Vue\n\nVue utilise des **directives** — des attributs spéciaux préfixés par `v-` — pour ajouter du comportement réactif au HTML.\n\n```vue\n\u003ctemplate\u003e\n  \u003cdiv\u003e\n    \u003c!-- v-bind (:) — Liaison d'attribut dynamique --\u003e\n    \u003cimg :src=\"urlImage\" :alt=\"description\" /\u003e\n    \u003ca :href=\"lien\" :class=\"{ actif: estActif }\"\u003eLien\u003c/a\u003e\n    \n    \u003c!-- v-if / v-else-if / v-else — Rendu conditionnel --\u003e\n    \u003cdiv v-if=\"note \u003e= 16\"\u003eMention Très Bien\u003c/div\u003e\n    \u003cdiv v-else-if=\"note \u003e= 14\"\u003eMention Bien\u003c/div\u003e\n    \u003cdiv v-else-if=\"note \u003e= 12\"\u003eMention Assez Bien\u003c/div\u003e\n    \u003cdiv v-else-if=\"note \u003e= 10\"\u003ePassable\u003c/div\u003e\n    \u003cdiv v-else\u003eAjourné\u003c/div\u003e\n    \n    \u003c!-- v-show — Affiche/cache avec CSS (display: none) --\u003e\n    \u003c!-- Contrairement à v-if, l'élément reste dans le DOM --\u003e\n    \u003cp v-show=\"afficherDetails\"\u003eDétails supplémentaires...\u003c/p\u003e\n    \n    \u003c!-- v-for — Boucle pour afficher une liste --\u003e\n    \u003c!-- :key est obligatoire pour aider Vue à optimiser le rendu --\u003e\n    \u003cul\u003e\n      \u003cli v-for=\"etudiant in etudiants\" :key=\"etudiant.id\"\u003e\n        {{ etudiant.nom }} — {{ etudiant.note }}/20\n      \u003c/li\u003e\n    \u003c/ul\u003e\n    \n    \u003c!-- v-for avec index --\u003e\n    \u003col\u003e\n      \u003cli v-for=\"(etudiant, index) in etudiants\" :key=\"etudiant.id\"\u003e\n        {{ index + 1 }}. {{ etudiant.nom }}\n      \u003c/li\u003e\n    \u003c/ol\u003e\n    \n    \u003c!-- v-on (@) — Écoute d'événements --\u003e\n    \u003cbutton @click=\"incrementer\"\u003eCliquer (+1)\u003c/button\u003e\n    \u003cbutton @click=\"compteur--\"\u003eDécrémenter (-1)\u003c/button\u003e\n    \u003cinput @keyup.enter=\"validerFormulaire\" /\u003e  \u003c!-- Modificateur .enter --\u003e\n    \n    \u003c!-- v-model — Liaison bidirectionnelle (two-way binding) --\u003e\n    \u003cinput v-model=\"recherche\" placeholder=\"Rechercher un étudiant...\" /\u003e\n    \u003cp\u003eVous cherchez : {{ recherche }}\u003c/p\u003e\n    \n    \u003c!-- v-model avec différents types d'input --\u003e\n    \u003ctextarea v-model=\"commentaire\"\u003e\u003c/textarea\u003e\n    \u003cselect v-model=\"filiereChoisie\"\u003e\n      \u003coption value=\"info\"\u003eInformatique\u003c/option\u003e\n      \u003coption value=\"math\"\u003eMathématiques\u003c/option\u003e\n    \u003c/select\u003e\n    \u003cinput type=\"checkbox\" v-model=\"accepte\" /\u003e\n  \u003c/div\u003e\n\u003c/template\u003e\n\n\u003cscript setup\u003e\nimport { ref } from 'vue'\n\nconst compteur = ref(0)\nconst recherche = ref('')\nconst commentaire = ref('')\nconst filiereChoisie = ref('info')\nconst accepte = ref(false)\n\nconst etudiants = ref([\n  { id: 1, nom: 'Alice', note: 16 },\n  { id: 2, nom: 'Bob', note: 12 },\n  { id: 3, nom: 'Charlie', note: 8 },\n])\n\nfunction incrementer() {\n  compteur.value++   // Avec ref(), on accède à la valeur via .value dans le script\n}\n\nfunction validerFormulaire() {\n  console.log('Formulaire validé !')\n}\n\u003c/script\u003e\n```\n\n### 6.5 Réactivité avec `ref` et `reactive`\n\nVue 3 propose deux fonctions principales pour créer des données réactives.\n\n```vue\n\u003cscript setup\u003e\nimport { ref, reactive, computed, watch } from 'vue'\n\n// ref() — pour les valeurs primitives (string, number, boolean)\n// Dans le \u003cscript\u003e, on accède à la valeur avec .value\n// Dans le \u003ctemplate\u003e, Vue déballe automatiquement (pas besoin de .value)\nconst compteur = ref(0)\nconst nom = ref('Alice')\ncompteur.value++           // Dans le script : .value obligatoire\n// Dans le template : {{ compteur }}  — pas de .value\n\n// reactive() — pour les objets et tableaux\n// Pas besoin de .value\nconst formulaire = reactive({\n  nom: '',\n  email: '',\n  filiere: 'informatique'\n})\nformulaire.nom = 'Bob'  // Modification directe, pas de .value\n\n// computed() — propriété calculée, se met à jour automatiquement\nconst longueurNom = computed(() =\u003e nom.value.length)\nconst estValide = computed(() =\u003e formulaire.nom.length \u003e 0 \u0026\u0026 formulaire.email.includes('@'))\n\n// watch() — observer des changements et réagir\nwatch(compteur, (nouvelleValeur, ancienneValeur) =\u003e {\n  console.log(`Compteur passé de ${ancienneValeur} à ${nouvelleValeur}`)\n})\n\n// Observation profonde d'un objet réactif\nwatch(\n  () =\u003e formulaire.nom,\n  (nouveauNom) =\u003e {\n    console.log(`Nom changé : ${nouveauNom}`)\n  }\n)\n\u003c/script\u003e\n```\n\n### 6.6 Émission d'événements (enfant vers parent)\n\n```vue\n\u003c!-- ===== Composant enfant : BarreRecherche.vue ===== --\u003e\n\u003ctemplate\u003e\n  \u003cdiv class=\"barre-recherche\"\u003e\n    \u003cinput \n      :value=\"modelValue\"\n      @input=\"$emit('update:modelValue', $event.target.value)\"\n      placeholder=\"Rechercher...\"\n    /\u003e\n    \u003cbutton @click=\"$emit('effacer')\"\u003eEffacer\u003c/button\u003e\n  \u003c/div\u003e\n\u003c/template\u003e\n\n\u003cscript setup\u003e\n// Déclaration des props\ndefineProps({\n  modelValue: String\n})\n\n// Déclaration des événements émis\ndefineEmits(['update:modelValue', 'effacer'])\n\u003c/script\u003e\n\n\u003c!-- ===== Composant parent ===== --\u003e\n\u003ctemplate\u003e\n  \u003cdiv\u003e\n    \u003c!-- v-model sur un composant personnalisé --\u003e\n    \u003cBarreRecherche v-model=\"termeRecherche\" @effacer=\"termeRecherche = ''\" /\u003e\n    \u003cp\u003eRecherche : {{ termeRecherche }}\u003c/p\u003e\n  \u003c/div\u003e\n\u003c/template\u003e\n\n\u003cscript setup\u003e\nimport { ref } from 'vue'\nimport BarreRecherche from './components/BarreRecherche.vue'\n\nconst termeRecherche = ref('')\n\u003c/script\u003e\n```\n\n### 6.7 Vue Router — Navigation\n\n```javascript\n// ===== fichier : src/router/index.js =====\n\nimport { createRouter, createWebHistory } from 'vue-router'\n\n// Import des composants de pages\nimport Accueil from '../views/Accueil.vue'\nimport ListeEtudiants from '../views/ListeEtudiants.vue'\n\nconst routes = [\n  {\n    path: '/',\n    name: 'accueil',\n    component: Accueil\n  },\n  {\n    path: '/etudiants',\n    name: 'etudiants',\n    component: ListeEtudiants\n  },\n  {\n    // Route dynamique avec paramètre\n    path: '/etudiant/:id',\n    name: 'detail-etudiant',\n    // Chargement paresseux (lazy loading) — le composant n'est chargé que quand on visite la route\n    component: () =\u003e import('../views/DetailEtudiant.vue')\n  },\n  {\n    // Route catch-all pour les pages 404\n    path: '/:pathMatch(.*)*',\n    name: 'not-found',\n    component: () =\u003e import('../views/PageNonTrouvee.vue')\n  }\n]\n\nconst router = createRouter({\n  history: createWebHistory(),\n  routes\n})\n\nexport default router\n```\n\n```vue\n\u003c!-- ===== fichier : src/App.vue ===== --\u003e\n\u003ctemplate\u003e\n  \u003cnav\u003e\n    \u003cRouterLink to=\"/\"\u003eAccueil\u003c/RouterLink\u003e\n    \u003cRouterLink to=\"/etudiants\"\u003eÉtudiants\u003c/RouterLink\u003e\n  \u003c/nav\u003e\n  \n  \u003c!-- Les composants de route s'affichent ici --\u003e\n  \u003cRouterView /\u003e\n\u003c/template\u003e\n```\n\n---\n\n## Chapitre 7 : Angular — Le framework complet\n\n### 7.1 Présentation\n\nAngular est un framework développé et maintenu par Google. Contrairement à React (bibliothèque légère) et Vue (framework progressif), Angular est une **plateforme complète** : elle inclut nativement tout ce dont on a besoin pour une application d'entreprise — routage, formulaires, client HTTP, injection de dépendances, tests, internationalisation.\n\nAngular utilise **TypeScript** comme langage principal (un sur-ensemble de JavaScript qui ajoute le typage statique). Cela peut sembler plus complexe au départ, mais c'est un avantage majeur pour les projets de grande envergure.\n\n### 7.2 Concepts clés propres à Angular\n\nAngular introduit plusieurs concepts architecturaux avancés.\n\n**Les modules (`@NgModule`)** organisent l'application en unités fonctionnelles. Chaque module déclare les composants, directives et services qu'il utilise. Les composants Angular autonomes (standalone) permettent maintenant de se passer de modules dans beaucoup de cas.\n\n**L'injection de dépendances** (DI) est un patron de conception où les dépendances d'un composant lui sont fournies de l'extérieur plutôt que créées en interne. Angular a un système de DI intégré très puissant.\n\n**Les services** encapsulent la logique métier et les appels API. Ils sont partagés entre les composants via l'injection de dépendances.\n\n**Les observables (RxJS)** sont au cœur d'Angular. Au lieu de Promesses, Angular utilise les Observables de la bibliothèque RxJS pour gérer les flux de données asynchrones.\n\n### 7.3 Installation et création d'un projet\n\n```bash\n# Installer Angular CLI globalement\nnpm install -g @angular/cli\n\n# Créer un nouveau projet\nng new mon-app-angular\n# Choisir : CSS, activer le routage\n\ncd mon-app-angular\n\n# Lancer le serveur de développement\nng serve\n# Accessible sur http://localhost:4200\n\n# Générer un composant\nng generate component components/carte-etudiant\n# ou en abrégé :\nng g c components/carte-etudiant\n\n# Générer un service\nng generate service services/etudiant\n```\n\n### 7.4 Un composant Angular\n\n```typescript\n// ===== fichier : src/app/components/carte-etudiant/carte-etudiant.component.ts =====\n\nimport { Component, Input } from '@angular/core';\nimport { CommonModule } from '@angular/common';\n\n// Le décorateur @Component configure le composant\n@Component({\n  selector: 'app-carte-etudiant',     // La balise HTML pour utiliser ce composant\n  standalone: true,                     // Composant autonome (Angular 17+)\n  imports: [CommonModule],              // Modules nécessaires\n  template: `\n    \u003cdiv class=\"carte\" [ngClass]=\"{ 'admis': estAdmis, 'ajourne': !estAdmis }\"\u003e\n      \u003ch3\u003e{{ nom }}\u003c/h3\u003e\n      \u003cp\u003eFilière : {{ filiere }}\u003c/p\u003e\n      \u003cp\u003eNote : {{ note }}/20\u003c/p\u003e\n      \u003cp class=\"statut\"\u003e\n        {{ estAdmis ? '✅ Admis(e)' : '❌ Ajourné(e)' }}\n      \u003c/p\u003e\n    \u003c/div\u003e\n  `,\n  styles: [`\n    .carte {\n      border: 1px solid #ddd;\n      border-radius: 8px;\n      padding: 16px;\n      margin: 8px;\n    }\n    .admis { background-color: #e8f5e9; }\n    .ajourne { background-color: #ffebee; }\n    .statut { font-weight: bold; }\n  `]\n})\nexport class CarteEtudiantComponent {\n  // @Input() déclare une propriété qui sera reçue du parent (équivalent des props)\n  @Input() nom: string = '';\n  @Input() filiere: string = '';\n  @Input() note: number = 0;\n\n  // Propriété calculée (getter)\n  get estAdmis(): boolean {\n    return this.note \u003e= 10;\n  }\n}\n```\n\nUtilisation dans le composant parent :\n\n```typescript\n// ===== fichier : src/app/app.component.ts =====\n\nimport { Component } from '@angular/core';\nimport { CarteEtudiantComponent } from './components/carte-etudiant/carte-etudiant.component';\nimport { CommonModule } from '@angular/common';\n\n@Component({\n  selector: 'app-root',\n  standalone: true,\n  imports: [CommonModule, CarteEtudiantComponent],\n  template: `\n    \u003ch1\u003eGestion des étudiants\u003c/h1\u003e\n    \u003cdiv class=\"grille\"\u003e\n      \u003c!-- Boucle *ngFor pour itérer sur la liste --\u003e\n      \u003capp-carte-etudiant\n        *ngFor=\"let etudiant of etudiants\"\n        [nom]=\"etudiant.nom\"\n        [filiere]=\"etudiant.filiere\"\n        [note]=\"etudiant.note\"\n      /\u003e\n    \u003c/div\u003e\n  `\n})\nexport class AppComponent {\n  etudiants = [\n    { nom: 'Alice Rakoto', filiere: 'Informatique', note: 16 },\n    { nom: 'Bob Rabe', filiere: 'Mathématiques', note: 8 },\n    { nom: 'Charlie Andria', filiere: 'Informatique', note: 14 },\n  ];\n}\n```\n\n### 7.5 Service et injection de dépendances\n\n```typescript\n// ===== fichier : src/app/services/etudiant.service.ts =====\n\nimport { Injectable } from '@angular/core';\nimport { HttpClient } from '@angular/common/http';\nimport { Observable } from 'rxjs';\n\n// Interface TypeScript pour typer les données\nexport interface Etudiant {\n  id: number;\n  nom: string;\n  filiere: string;\n  note: number;\n}\n\n// @Injectable rend ce service injectable dans n'importe quel composant\n// providedIn: 'root' → une seule instance partagée dans toute l'application (singleton)\n@Injectable({\n  providedIn: 'root'\n})\nexport class EtudiantService {\n  private apiUrl = 'http://localhost:3000/api/etudiants';\n\n  // HttpClient est injecté automatiquement par Angular\n  constructor(private http: HttpClient) {}\n\n  // Les méthodes retournent des Observables (pas des Promesses)\n  getTous(): Observable\u003cEtudiant[]\u003e {\n    return this.http.get\u003cEtudiant[]\u003e(this.apiUrl);\n  }\n\n  getParId(id: number): Observable\u003cEtudiant\u003e {\n    return this.http.get\u003cEtudiant\u003e(`${this.apiUrl}/${id}`);\n  }\n\n  creer(etudiant: Omit\u003cEtudiant, 'id'\u003e): Observable\u003cEtudiant\u003e {\n    return this.http.post\u003cEtudiant\u003e(this.apiUrl, etudiant);\n  }\n\n  modifier(id: number, etudiant: Partial\u003cEtudiant\u003e): Observable\u003cEtudiant\u003e {\n    return this.http.put\u003cEtudiant\u003e(`${this.apiUrl}/${id}`, etudiant);\n  }\n\n  supprimer(id: number): Observable\u003cvoid\u003e {\n    return this.http.delete\u003cvoid\u003e(`${this.apiUrl}/${id}`);\n  }\n}\n```\n\n```typescript\n// ===== Utilisation du service dans un composant =====\n\nimport { Component, OnInit } from '@angular/core';\nimport { EtudiantService, Etudiant } from '../services/etudiant.service';\n\n@Component({\n  selector: 'app-liste-etudiants',\n  standalone: true,\n  template: `\n    \u003cdiv *ngIf=\"chargement\"\u003eChargement...\u003c/div\u003e\n    \u003cdiv *ngIf=\"erreur\" class=\"erreur\"\u003e{{ erreur }}\u003c/div\u003e\n    \n    \u003cul *ngIf=\"!chargement \u0026\u0026 !erreur\"\u003e\n      \u003cli *ngFor=\"let e of etudiants\"\u003e\n        {{ e.nom }} — {{ e.filiere }} — {{ e.note }}/20\n      \u003c/li\u003e\n    \u003c/ul\u003e\n  `\n})\nexport class ListeEtudiantsComponent implements OnInit {\n  etudiants: Etudiant[] = [];\n  chargement = true;\n  erreur: string | null = null;\n\n  // Le service est injecté via le constructeur\n  constructor(private etudiantService: EtudiantService) {}\n\n  // ngOnInit est le hook de cycle de vie appelé après l'initialisation\n  ngOnInit(): void {\n    this.etudiantService.getTous().subscribe({\n      next: (donnees) =\u003e {\n        this.etudiants = donnees;\n        this.chargement = false;\n      },\n      error: (err) =\u003e {\n        this.erreur = 'Erreur de chargement';\n        this.chargement = false;\n      }\n    });\n  }\n}\n```\n\n---\n\n## Chapitre 8 : Comparaison et choix d'un framework frontend\n\n### 8.1 Tableau comparatif synthétique\n\n| Critère | React | Vue.js | Angular |\n|---------|-------|--------|---------|\n| **Type** | Bibliothèque | Framework progressif | Plateforme complète |\n| **Langage** | JavaScript/JSX | JavaScript/SFC | TypeScript |\n| **Courbe d'apprentissage** | Moyenne | Douce | Raide |\n| **Taille du bundle** | ~40 Ko | ~30 Ko | ~150 Ko |\n| **DOM virtuel** | Oui | Oui | Non (détection de changements) |\n| **Data binding** | Unidirectionnel | Bidirectionnel (v-model) | Bidirectionnel |\n| **Écosystème** | Très vaste, fragmenté | Modéré, cohérent | Intégré, complet |\n| **Mainteneur** | Meta (Facebook) | Communauté (Evan You) | Google |\n| **Adapté pour** | Tout type de projet | Petits à grands projets | Applications d'entreprise |\n| **Part de marché** | Dominant | En croissance | Stable |\n\n### 8.2 Critères de choix\n\nLe choix d'un framework dépend du contexte. Pour un **projet personnel ou startup**, React ou Vue conviennent parfaitement grâce à leur flexibilité et leur rapidité de développement. Pour une **application d'entreprise** avec une grande équipe, Angular offre une structure imposée qui facilite la collaboration. Pour un **débutant**, Vue est souvent recommandé pour sa documentation et sa simplicité. Pour le **marché de l'emploi**, React domine largement en nombre d'offres.\n\n---\n\n# Partie II — Le Backend\n\n---\n\n## Chapitre 9 : Node.js — JavaScript côté serveur\n\n### 9.1 Qu'est-ce que Node.js ?\n\nNode.js est un **environnement d'exécution** (runtime) qui permet d'exécuter du JavaScript en dehors du navigateur. Créé en 2009 par Ryan Dahl, il repose sur le moteur **V8** de Google Chrome, le même moteur qui exécute JavaScript dans le navigateur Chrome.\n\nAvant Node.js, JavaScript ne pouvait s'exécuter que dans un navigateur. Le développement côté serveur nécessitait un autre langage (PHP, Java, Python, Ruby). Node.js a changé cette donne : désormais, un développeur peut utiliser JavaScript pour le frontend **et** le backend, réduisant la barrière entre les deux.\n\n### 9.2 Architecture événementielle non bloquante\n\nLa caractéristique fondamentale de Node.js est son modèle d'exécution **mono-thread, événementiel et non bloquant** (single-threaded, event-driven, non-blocking I/O).\n\nPour comprendre ce concept, comparons avec un modèle traditionnel (comme PHP avec Apache) :\n\n**Modèle traditionnel (multi-thread bloquant) :**\n```\nClient A fait une requête → Serveur crée un Thread A → Thread A attend la base de données... → Réponse\nClient B fait une requête → Serveur crée un Thread B → Thread B attend la base de données... → Réponse\nClient C fait une requête → Serveur crée un Thread C → ...\n// Chaque requête bloque un thread. 10 000 clients = 10 000 threads en mémoire\n```\n\n**Modèle Node.js (mono-thread non bloquant) :**\n```\nClient A fait une requête → Node envoie la requête à la BD (sans attendre) → passe au suivant\nClient B fait une requête → Node envoie la requête à la BD (sans attendre) → passe au suivant\nClient C fait une requête → ...\nBD répond pour Client A → Node envoie la réponse à Client A\n// Un seul thread gère des milliers de connexions simultanées\n```\n\nNode.js utilise une **boucle d'événements** (Event Loop) qui traite les opérations de manière asynchrone. Les opérations longues (lecture de fichiers, requêtes réseau, accès base de données) sont déléguées au système d'exploitation, et Node.js est notifié quand elles sont terminées.\n\n### 9.3 npm — Le gestionnaire de paquets\n\n**npm** (Node Package Manager) est le gestionnaire de paquets de Node.js et le plus grand registre de bibliothèques open source au monde. Chaque projet Node.js contient un fichier `package.json` qui décrit le projet et ses dépendances.\n\n```bash\n# Initialiser un nouveau projet\nnpm init -y\n\n# Installer une dépendance de production\nnpm install express\n\n# Installer une dépendance de développement (tests, linting, etc.)\nnpm install --save-dev nodemon\n\n# Installer globalement (outils en ligne de commande)\nnpm install -g typescript\n```\n\n```json\n// package.json — fichier central de tout projet Node.js\n{\n  \"name\": \"mon-api-universitaire\",\n  \"version\": \"1.0.0\",\n  \"description\": \"API pour la gestion universitaire\",\n  \"main\": \"src/server.js\",\n  \"scripts\": {\n    \"start\": \"node src/server.js\",\n    \"dev\": \"nodemon src/server.js\",\n    \"test\": \"jest\"\n  },\n  \"dependencies\": {\n    \"express\": \"^4.18.2\",\n    \"mongoose\": \"^7.5.0\",\n    \"cors\": \"^2.8.5\"\n  },\n  \"devDependencies\": {\n    \"nodemon\": \"^3.0.0\",\n    \"jest\": \"^29.0.0\"\n  }\n}\n```\n\n### 9.4 Premier serveur HTTP avec Node.js pur\n\n```javascript\n// ===== fichier : server.js =====\n\n// Importation du module http intégré à Node.js\nconst http = require('http');\n\n// Définition du port\nconst PORT = 3000;\n\n// Création du serveur\nconst serveur = http.createServer((requete, reponse) =\u003e {\n  // requete contient les informations de la requête HTTP\n  // reponse permet d'envoyer la réponse au client\n  \n  console.log(`${requete.method} ${requete.url}`);\n  \n  // Configuration des en-têtes de la réponse\n  reponse.setHeader('Content-Type', 'application/json; charset=utf-8');\n  \n  // Routage basique\n  if (requete.url === '/' \u0026\u0026 requete.method === 'GET') {\n    reponse.statusCode = 200;\n    reponse.end(JSON.stringify({ \n      message: 'Bienvenue sur l\\'API universitaire !',\n      version: '1.0.0'\n    }));\n  } \n  else if (requete.url === '/etudiants' \u0026\u0026 requete.method === 'GET') {\n    const etudiants = [\n      { id: 1, nom: 'Alice Rakoto', filiere: 'Informatique' },\n      { id: 2, nom: 'Bob Rabe', filiere: 'Mathématiques' },\n    ];\n    reponse.statusCode = 200;\n    reponse.end(JSON.stringify(etudiants));\n  }\n  else {\n    reponse.statusCode = 404;\n    reponse.end(JSON.stringify({ erreur: 'Route non trouvée' }));\n  }\n});\n\n// Le serveur écoute sur le port spécifié\nserveur.listen(PORT, () =\u003e {\n  console.log(`Serveur démarré sur http://localhost:${PORT}`);\n});\n```\n\nCe code fonctionne, mais on voit immédiatement le problème : gérer le routage manuellement devient très vite ingérable. C'est pourquoi on utilise un framework comme Express.js.\n\n---\n\n## Chapitre 10 : Express.js — Le framework backend de référence\n\n### 10.1 Présentation\n\nExpress.js est le framework backend le plus populaire de l'écosystème Node.js. Créé en 2010 par TJ Holowaychuk, c'est un framework **minimaliste et flexible** qui fournit les outils essentiels pour construire des applications web et des API.\n\nExpress simplifie considérablement le développement côté serveur en fournissant un système de routage élégant, un mécanisme de middleware puissant, la gestion des requêtes et réponses HTTP, et la possibilité de servir des fichiers statiques.\n\n### 10.2 Installation et premier serveur\n\n```bash\nmkdir mon-api\ncd mon-api\nnpm init -y\nnpm install express\nnpm install --save-dev nodemon\n```\n\n```javascript\n// ===== fichier : src/server.js =====\n\nconst express = require('express');\n\n// Création de l'application Express\nconst app = express();\nconst PORT = 3000;\n\n// Middleware pour parser le JSON dans le corps des requêtes\napp.use(express.json());\n\n// Middleware pour parser les données de formulaires URL-encoded\napp.use(express.urlencoded({ extended: true }));\n\n// Route GET sur la racine\napp.get('/', (req, res) =\u003e {\n  res.json({ \n    message: 'Bienvenue sur l\\'API universitaire !',\n    version: '1.0.0',\n    endpoints: {\n      etudiants: '/api/etudiants',\n      filieres: '/api/filieres'\n    }\n  });\n});\n\n// Démarrage du serveur\napp.listen(PORT, () =\u003e {\n  console.log(`Serveur Express démarré sur http://localhost:${PORT}`);\n});\n```\n\n### 10.3 Le concept de Middleware\n\nUn **middleware** est une fonction qui a accès à l'objet requête (`req`), l'objet réponse (`res`), et à la fonction `next()` qui passe le contrôle au middleware suivant. Les middlewares forment une **chaîne de traitement** : chaque requête traverse une série de middlewares avant d'atteindre le gestionnaire de route final.\n\n```\nRequête HTTP →  [Middleware 1: Logger]  →  [Middleware 2: Auth]  →  [Route Handler]  → Réponse\n```\n\n```javascript\n// ===== Exemples de middlewares =====\n\n// 1. Middleware de journalisation (logging) — s'applique à TOUTES les requêtes\napp.use((req, res, next) =\u003e {\n  const debut = Date.now();\n  \n  // On écoute la fin de la réponse pour calculer la durée\n  res.on('finish', () =\u003e {\n    const duree = Date.now() - debut;\n    console.log(`[${new Date().toISOString()}] ${req.method} ${req.url} → ${res.statusCode} (${duree}ms)`);\n  });\n  \n  next(); // IMPORTANT : appeler next() pour passer au middleware suivant\n});\n\n// 2. Middleware CORS (Cross-Origin Resource Sharing)\n// Permet aux frontends hébergés sur un autre domaine d'accéder à l'API\nconst cors = require('cors');\napp.use(cors({\n  origin: 'http://localhost:5173', // URL du frontend Vite\n  methods: ['GET', 'POST', 'PUT', 'DELETE'],\n  credentials: true\n}));\n\n// 3. Middleware d'authentification personnalisé\nfunction verifierAuthentification(req, res, next) {\n  const token = req.headers.authorization;\n  \n  if (!token) {\n    return res.status(401).json({ erreur: 'Token d\\'authentification requis' });\n    // Pas de next() → la chaîne s'arrête ici\n  }\n  \n  try {\n    // Vérification du token (simplifié)\n    const utilisateur = verifierToken(token);\n    req.utilisateur = utilisateur; // On ajoute l'utilisateur à la requête\n    next(); // Token valide → on continue\n  } catch (err) {\n    return res.status(403).json({ erreur: 'Token invalide' });\n  }\n}\n\n// Application du middleware uniquement sur certaines routes\napp.get('/api/profil', verifierAuthentification, (req, res) =\u003e {\n  res.json({ utilisateur: req.utilisateur });\n});\n```\n\n### 10.4 Routage\n\n```javascript\n// ===== fichier : src/routes/etudiants.js =====\n\nconst express = require('express');\nconst router = express.Router();\n\n// Données en mémoire (en production, on utiliserait une base de données)\nlet etudiants = [\n  { id: 1, nom: 'Alice Rakoto', filiere: 'Informatique', note: 16 },\n  { id: 2, nom: 'Bob Rabe', filiere: 'Mathématiques', note: 12 },\n  { id: 3, nom: 'Charlie Andria', filiere: 'Informatique', note: 14 },\n  { id: 4, nom: 'Diana Razafy', filiere: 'Physique', note: 9 },\n];\n\nlet prochainId = 5;\n\n// GET /api/etudiants — Récupérer tous les étudiants\n// Supporte les filtres par query string : ?filiere=Informatique\u0026noteMin=10\nrouter.get('/', (req, res) =\u003e {\n  let resultat = [...etudiants];\n  \n  // Filtrage par filière\n  if (req.query.filiere) {\n    resultat = resultat.filter(e =\u003e \n      e.filiere.toLowerCase() === req.query.filiere.toLowerCase()\n    );\n  }\n  \n  // Filtrage par note minimum\n  if (req.query.noteMin) {\n    resultat = resultat.filter(e =\u003e e.note \u003e= parseInt(req.query.noteMin));\n  }\n  \n  res.json({\n    total: resultat.length,\n    etudiants: resultat\n  });\n});\n\n// GET /api/etudiants/:id — Récupérer un étudiant par son ID\nrouter.get('/:id', (req, res) =\u003e {\n  const id = parseInt(req.params.id);\n  const etudiant = etudiants.find(e =\u003e e.id === id);\n  \n  if (!etudiant) {\n    return res.status(404).json({ erreur: `Étudiant avec l'ID ${id} non trouvé` });\n  }\n  \n  res.json(etudiant);\n});\n\n// POST /api/etudiants — Créer un nouvel étudiant\nrouter.post('/', (req, res) =\u003e {\n  const { nom, filiere, note } = req.body;\n  \n  // Validation des données\n  const erreurs = [];\n  if (!nom || nom.trim().length === 0) erreurs.push('Le nom est requis.');\n  if (!filiere) erreurs.push('La filière est requise.');\n  if (note === undefined || note \u003c 0 || note \u003e 20) erreurs.push('La note doit être entre 0 et 20.');\n  \n  if (erreurs.length \u003e 0) {\n    return res.status(400).json({ erreurs });\n  }\n  \n  const nouvelEtudiant = {\n    id: prochainId++,\n    nom: nom.trim(),\n    filiere,\n    note: parseFloat(note)\n  };\n  \n  etudiants.push(nouvelEtudiant);\n  \n  // 201 Created — la ressource a été créée avec succès\n  res.status(201).json(nouvelEtudiant);\n});\n\n// PUT /api/etudiants/:id — Modifier un étudiant existant (remplacement complet)\nrouter.put('/:id', (req, res) =\u003e {\n  const id = parseInt(req.params.id);\n  const index = etudiants.findIndex(e =\u003e e.id === id);\n  \n  if (index === -1) {\n    return res.status(404).json({ erreur: `Étudiant avec l'ID ${id} non trouvé` });\n  }\n  \n  const { nom, filiere, note } = req.body;\n  \n  etudiants[index] = { id, nom, filiere, note: parseFloat(note) };\n  \n  res.json(etudiants[index]);\n});\n\n// DELETE /api/etudiants/:id — Supprimer un étudiant\nrouter.delete('/:id', (req, res) =\u003e {\n  const id = parseInt(req.params.id);\n  const index = etudiants.findIndex(e =\u003e e.id === id);\n  \n  if (index === -1) {\n    return res.status(404).json({ erreur: `Étudiant avec l'ID ${id} non trouvé` });\n  }\n  \n  const supprime = etudiants.splice(index, 1)[0];\n  \n  res.json({ message: `Étudiant \"${supprime.nom}\" supprimé avec succès` });\n});\n\nmodule.exports = router;\n\n// ===== fichier : src/server.js — Enregistrement des routes =====\n\nconst express = require('express');\nconst cors = require('cors');\nconst etudiantsRoutes = require('./routes/etudiants');\n\nconst app = express();\n\napp.use(cors());\napp.use(express.json());\n\n// Montage du routeur sur le préfixe /api/etudiants\napp.use('/api/etudiants', etudiantsRoutes);\n\n// Middleware de gestion des erreurs (toujours en dernier)\napp.use((err, req, res, next) =\u003e {\n  console.error('Erreur serveur :', err.stack);\n  res.status(500).json({ erreur: 'Erreur interne du serveur' });\n});\n\napp.listen(3000, () =\u003e {\n  console.log('Serveur démarré sur http://localhost:3000');\n});\n```\n\n---\n\n## Chapitre 11 : API RESTful — Conception et implémentation\n\n### 11.1 Les principes REST\n\n**REST** (Representational State Transfer) est un style d'architecture pour la conception d'API web. Ce n'est pas un protocole ni un standard, mais un ensemble de contraintes et de bonnes pratiques définies par Roy Fielding dans sa thèse de doctorat en 2000.\n\nLes principes fondamentaux de REST sont les suivants.\n\n**Client-Serveur** : Le client (frontend) et le serveur (backend) sont séparés et communiquent via HTTP. Chacun peut évoluer indépendamment.\n\n**Sans état (Stateless)** : Chaque requête du client contient toute l'information nécessaire. Le serveur ne conserve pas d'état de session entre les requêtes.\n\n**Ressources identifiées par des URI** : Chaque entité (étudiant, cours, note) est une ressource accessible via une URL unique.\n\n**Méthodes HTTP standard** : Les opérations CRUD (Create, Read, Update, Delete) correspondent aux méthodes HTTP.\n\n| Opération | Méthode HTTP | URI | Description |\n|-----------|-------------|-----|-------------|\n| Créer | POST | `/api/etudiants` | Crée un nouvel étudiant |\n| Lire tous | GET | `/api/etudiants` | Récupère la liste des étudiants |\n| Lire un | GET | `/api/etudiants/42` | Récupère l'étudiant d'ID 42 |\n| Modifier | PUT | `/api/etudiants/42` | Modifie entièrement l'étudiant 42 |\n| Modifier partiellement | PATCH | `/api/etudiants/42` | Modifie certains champs de l'étudiant 42 |\n| Supprimer | DELETE | `/api/etudiants/42` | Supprime l'étudiant 42 |\n\n### 11.2 Codes de statut HTTP\n\nLes codes de statut indiquent le résultat de la requête. Ils se répartissent en 5 catégories :\n\n| Code | Catégorie | Signification | Utilisation courante |\n|------|-----------|---------------|---------------------|\n| 200 | Succès | OK | Requête GET ou PUT réussie |\n| 201 | Succès | Created | Ressource créée (POST réussi) |\n| 204 | Succès | No Content | Suppression réussie (pas de corps de réponse) |\n| 400 | Erreur client | Bad Request | Données invalides envoyées par le client |\n| 401 | Erreur client | Unauthorized | Authentification requise |\n| 403 | Erreur client | Forbidden | Authentifié mais pas autorisé |\n| 404 | Erreur client | Not Found | Ressource inexistante |\n| 409 | Erreur client | Conflict | Conflit (ex : email déjà utilisé) |\n| 422 | Erreur client | Unprocessable Entity | Données valides syntaxiquement mais sémantiquement incorrectes |\n| 500 | Erreur serveur | Internal Server Error | Erreur inattendue côté serveur |\n\n### 11.3 Bonnes pratiques de conception d'API REST\n\n```\n# Conventions de nommage des routes\n\n# ✅ CORRECT — noms pluriels, tout en minuscules, tirets pour les mots composés\nGET    /api/etudiants\nGET    /api/etudiants/42\nPOST   /api/etudiants\nGET    /api/etudiants/42/notes            # sous-ressource : notes de l'étudiant 42\nGET    /api/annees-universitaires          # tirets, pas de camelCase ni underscore\n\n# ❌ INCORRECT\nGET    /api/getEtudiants                  # pas de verbes dans l'URL\nGET    /api/etudiant                      # utiliser le pluriel\nGET    /api/Etudiants                     # pas de majuscules\nGET    /api/etudiants/supprimer/42        # pas de verbes (utiliser DELETE)\n```\n\n---\n\n## Chapitre 12 : Connexion à une base de données\n\n### 12.1 MongoDB avec Mongoose\n\nMongoDB est une base de données **NoSQL** orientée documents. Les données sont stockées sous forme de documents JSON (techniquement BSON). **Mongoose** est une bibliothèque ODM (Object Document Mapper) qui facilite l'interaction avec MongoDB depuis Node.js.\n\n```bash\nnpm install mongoose\n```\n\n```javascript\n// ===== fichier : src/config/database.js =====\n\nconst mongoose = require('mongoose');\n\nconst connecterBD = async () =\u003e {\n  try {\n    await mongoose.connect('mongodb://localhost:27017/universite', {\n      // Options recommandées (gérées automatiquement dans les versions récentes)\n    });\n    console.log('✅ Connecté à MongoDB avec succès');\n  } catch (erreur) {\n    console.error('❌ Erreur de connexion à MongoDB :', erreur.message);\n    process.exit(1); // Arrête le serveur si la BD est inaccessible\n  }\n};\n\nmodule.exports = connecterBD;\n```\n\n```javascript\n// ===== fichier : src/models/Etudiant.js =====\n\nconst mongoose = require('mongoose');\n\n// Le schéma définit la structure des documents dans la collection\nconst schemaEtudiant = new mongoose.Schema({\n  nom: {\n    type: String,\n    required: [true, 'Le nom est obligatoire'],\n    trim: true,                    // Supprime les espaces en début/fin\n    minlength: [2, 'Le nom doit contenir au moins 2 caractères'],\n    maxlength: [100, 'Le nom ne peut pas dépasser 100 caractères']\n  },\n  email: {\n    type: String,\n    required: [true, 'L\\'email est obligatoire'],\n    unique: true,                   // Index unique — pas de doublons\n    lowercase: true,                // Convertit en minuscules automatiquement\n    match: [/^\\S+@\\S+\\.\\S+$/, 'Format d\\'email invalide']\n  },\n  filiere: {\n    type: String,\n    required: true,\n    enum: {\n      values: ['Informatique', 'Mathématiques', 'Physique', 'Chimie'],\n      message: 'Filière \"{VALUE}\" non reconnue'\n    }\n  },\n  note: {\n    type: Number,\n    min: [0, 'La note ne peut pas être négative'],\n    max: [20, 'La note ne peut pas dépasser 20'],\n    default: 0\n  },\n  dateInscription: {\n    type: Date,\n    default: Date.now\n  },\n  actif: {\n    type: Boolean,\n    default: true\n  }\n}, {\n  timestamps: true  // Ajoute automatiquement createdAt et updatedAt\n});\n\n// Propriété virtuelle (non stockée en BD, calculée à la volée)\nschemaEtudiant.virtual('estAdmis').get(function() {\n  return this.note \u003e= 10;\n});\n\n// S'assurer que les virtuels apparaissent dans le JSON\nschemaEtudiant.set('toJSON', { virtuals: true });\n\n// Méthode d'instance\nschemaEtudiant.methods.getMention = function() {\n  if (this.note \u003e= 16) return 'Très Bien';\n  if (this.note \u003e= 14) return 'Bien';\n  if (this.note \u003e= 12) return 'Assez Bien';\n  if (this.note \u003e= 10) return 'Passable';\n  return 'Ajourné';\n};\n\n// Méthode statique\nschemaEtudiant.statics.trouverParFiliere = function(filiere) {\n  return this.find({ filiere, actif: true }).sort({ nom: 1 });\n};\n\n// Création et export du modèle\n// 'Etudiant' → collection 'etudiants' dans MongoDB\nconst Etudiant = mongoose.model('Etudiant', schemaEtudiant);\n\nmodule.exports = Etudiant;\n```\n\n```javascript\n// ===== fichier : src/routes/etudiants.js — version avec MongoDB =====\n\nconst express = require('express');\nconst router = express.Router();\nconst Etudiant = require('../models/Etudiant');\n\n// GET /api/etudiants — Récupérer tous les étudiants avec filtrage et pagination\nrouter.get('/', async (req, res) =\u003e {\n  try {\n    const { filiere, noteMin, page = 1, limite = 10, tri = 'nom' } = req.query;\n    \n    // Construction du filtre dynamique\n    const filtre = { actif: true };\n    if (filiere) filtre.filiere = filiere;\n    if (noteMin) filtre.note = { $gte: parseFloat(noteMin) };\n    \n    // Pagination\n    const skip = (parseInt(page) - 1) * parseInt(limite);\n    \n    // Exécution de la requête\n    const [etudiants, total] = await Promise.all([\n      Etudiant.find(filtre)\n        .sort(tri)\n        .skip(skip)\n        .limit(parseInt(limite)),\n      Etudiant.countDocuments(filtre)\n    ]);\n    \n    res.json({\n      total,\n      page: parseInt(page),\n      totalPages: Math.ceil(total / parseInt(limite)),\n      etudiants\n    });\n  } catch (erreur) {\n    res.status(500).json({ erreur: erreur.message });\n  }\n});\n\n// GET /api/etudiants/:id\nrouter.get('/:id', async (req, res) =\u003e {\n  try {\n    const etudiant = await Etudiant.findById(req.params.id);\n    \n    if (!etudiant) {\n      return res.status(404).json({ erreur: 'Étudiant non trouvé' });\n    }\n    \n    res.json(etudiant);\n  } catch (erreur) {\n    // Gestion de l'erreur d'ID invalide (format MongoDB)\n    if (erreur.name === 'CastError') {\n      return res.status(400).json({ erreur: 'Format d\\'ID invalide' });\n    }\n    res.status(500).json({ erreur: erreur.message });\n  }\n});\n\n// POST /api/etudiants\nrouter.post('/', async (req, res) =\u003e {\n  try {\n    const etudiant = new Etudiant(req.body);\n    const sauvegarde = await etudiant.save();\n    res.status(201).json(sauvegarde);\n  } catch (erreur) {\n    // Erreur de validation Mongoose\n    if (erreur.name === 'ValidationError') {\n      const messages = Object.values(erreur.errors).map(e =\u003e e.message);\n      return res.status(400).json({ erreurs: messages });\n    }\n    // Erreur de doublon (email unique)\n    if (erreur.code === 11000) {\n      return res.status(409).json({ erreur: 'Cet email est déjà utilisé' });\n    }\n    res.status(500).json({ erreur: erreur.message });\n  }\n});\n\n// PUT /api/etudiants/:id\nrouter.put('/:id', async (req, res) =\u003e {\n  try {\n    const etudiant = await Etudiant.findByIdAndUpdate(\n      req.params.id,\n      req.body,\n      { new: true, runValidators: true } // retourne le document modifié + valide les données\n    );\n    \n    if (!etudiant) {\n      return res.status(404).json({ erreur: 'Étudiant non trouvé' });\n    }\n    \n    res.json(etudiant);\n  } catch (erreur) {\n    if (erreur.name === 'ValidationError') {\n      const messages = Object.values(erreur.errors).map(e =\u003e e.message);\n      return res.status(400).json({ erreurs: messages });\n    }\n    res.status(500).json({ erreur: erreur.message });\n  }\n});\n\n// DELETE /api/etudiants/:id — Suppression logique (soft delete)\nrouter.delete('/:id', async (req, res) =\u003e {\n  try {\n    const etudiant = await Etudiant.findByIdAndUpdate(\n      req.params.id,\n      { actif: false },\n      { new: true }\n    );\n    \n    if (!etudiant) {\n      return res.status(404).json({ erreur: 'Étudiant non trouvé' });\n    }\n    \n    res.json({ message: `Étudiant \"${etudiant.nom}\" désactivé avec succès` });\n  } catch (erreur) {\n    res.status(500).json({ erreur: erreur.message });\n  }\n});\n\nmodule.exports = router;\n```\n\n### 12.2 SQL avec Sequelize (MySQL/PostgreSQL)\n\nPour les bases de données relationnelles, **Sequelize** est l'ORM (Object-Relational Mapper) le plus utilisé avec Node.js.\n\n```bash\nnpm install sequelize mysql2   # Pour MySQL\n# ou\nnpm install sequelize pg       # Pour PostgreSQL\n```\n\n```javascript\n// ===== fichier : src/models/Etudiant.js — version Sequelize =====\n\nconst { Sequelize, DataTypes, Model } = require('sequelize');\n\n// Connexion à la base de données\nconst sequelize = new Sequelize('universite', 'root', 'mot_de_passe', {\n  host: 'localhost',\n  dialect: 'mysql',   // ou 'postgres'\n  logging: false       // Désactive les logs SQL en console\n});\n\n// Définition du modèle\nclass Etudiant extends Model {\n  // Méthode d'instance\n  getMention() {\n    if (this.note \u003e= 16) return 'Très Bien';\n    if (this.note \u003e= 14) return 'Bien';\n    if (this.note \u003e= 12) return 'Assez Bien';\n    if (this.note \u003e= 10) return 'Passable';\n    return 'Ajourné';\n  }\n}\n\nEtudiant.init({\n  nom: {\n    type: DataTypes.STRING(100),\n    allowNull: false,\n    validate: {\n      notEmpty: { msg: 'Le nom est requis' },\n      len: { args: [2, 100], msg: 'Le nom doit contenir entre 2 et 100 caractères' }\n    }\n  },\n  email: {\n    type: DataTypes.STRING,\n    allowNull: false,\n    unique: true,\n    validate: {\n      isEmail: { msg: 'Format d\\'email invalide' }\n    }\n  },\n  filiere: {\n    type: DataTypes.ENUM('Informatique', 'Mathématiques', 'Physique', 'Chimie'),\n    allowNull: false\n  },\n  note: {\n    type: DataTypes.FLOAT,\n    defaultValue: 0,\n    validate: {\n      min: { args: [0], msg: 'La note ne peut pas être négative' },\n      max: { args: [20], msg: 'La note ne peut pas dépasser 20' }\n    }\n  },\n  actif: {\n    type: DataTypes.BOOLEAN,\n    defaultValue: true\n  }\n}, {\n  sequelize,\n  modelName: 'Etudiant',\n  tableName: 'etudiants',\n  timestamps: true      // createdAt et updatedAt automatiques\n});\n\nmodule.exports = { sequelize, Etudiant };\n```\n\n---\n\n## Chapitre 13 : Authentification et sécurité\n\n### 13.1 Authentification par JWT (JSON Web Token)\n\nLe **JWT** est le mécanisme d'authentification le plus répandu dans les API REST modernes. Il fonctionne selon le principe suivant :\n\n```\n1. L'utilisateur envoie ses identifiants (email + mot de passe)\n2. Le serveur vérifie les identifiants\n3. Si valides → le serveur génère un TOKEN JWT et le renvoie au client\n4. Le client stocke le token et l'envoie dans l'en-tête de chaque requête suivante\n5. Le serveur vérifie le token à chaque requête protégée\n```\n\nUn JWT est composé de trois parties séparées par des points : `HEADER.PAYLOAD.SIGNATURE`. Le header contient le type de token et l'algorithme. Le payload contient les données (ID utilisateur, rôle, date d'expiration). La signature garantit que le token n'a pas été modifié.\n\n```bash\nnpm install jsonwebtoken bcryptjs\n```\n\n```javascript\n// ===== fichier : src/models/Utilisateur.js =====\n\nconst mongoose = require('mongoose');\nconst bcrypt = require('bcryptjs');\n\nconst schemaUtilisateur = new mongoose.Schema({\n  nom: { type: String, required: true, trim: true },\n  email: { type: String, required: true, unique: true, lowercase: true },\n  motDePasse: { type: String, required: true, minlength: 6 },\n  role: { type: String, enum: ['etudiant', 'enseignant', 'admin'], default: 'etudiant' }\n}, { timestamps: true });\n\n// Hook \"pre-save\" — hache le mot de passe avant la sauvegarde\nschemaUtilisateur.pre('save', async function(next) {\n  // Ne hacher que si le mot de passe a été modifié\n  if (!this.isModified('motDePasse')) return next();\n  \n  // Génération du sel et hachage\n  const sel = await bcrypt.genSalt(10);\n  this.motDePasse = await bcrypt.hash(this.motDePasse, sel);\n  next();\n});\n\n// Méthode pour vérifier le mot de passe\nschemaUtilisateur.methods.verifierMotDePasse = async function(motDePasseCandidat) {\n  return bcrypt.compare(motDePasseCandidat, this.motDePasse);\n};\n\n// Ne jamais renvoyer le mot de passe dans le JSON\nschemaUtilisateur.methods.toJSON = function() {\n  const obj = this.toObject();\n  delete obj.motDePasse;\n  return obj;\n};\n\nmodule.exports = mongoose.model('Utilisateur', schemaUtilisateur);\n```\n\n```javascript\n// ===== fichier : src/routes/auth.js =====\n\nconst express = require('express');\nconst jwt = require('jsonwebtoken');\nconst Utilisateur = require('../models/Utilisateur');\n\nconst router = express.Router();\n\n// Clé secrète pour signer les tokens (en production, utiliser une variable d'environnement)\nconst JWT_SECRET = process.env.JWT_SECRET || 'ma_cle_secrete_tres_longue_et_complexe';\nconst JWT_EXPIRE = '24h';\n\n// Fonction utilitaire pour générer un token\nfunction genererToken(utilisateur) {\n  return jwt.sign(\n    { id: utilisateur._id, email: utilisateur.email, role: utilisateur.role },\n    JWT_SECRET,\n    { expiresIn: JWT_EXPIRE }\n  );\n}\n\n// POST /api/auth/inscription\nrouter.post('/inscription', async (req, res) =\u003e {\n  try {\n    const { nom, email, motDePasse, role } = req.body;\n    \n    // Vérifier si l'utilisateur existe déjà\n    const existant = await Utilisateur.findOne({ email });\n    if (existant) {\n      return res.status(409).json({ erreur: 'Cet email est déjà utilisé' });\n    }\n    \n    // Créer le nouvel utilisateur\n    const utilisateur = new Utilisateur({ nom, email, motDePasse, role });\n    await utilisateur.save();\n    \n    // Générer le token\n    const token = genererToken(utilisateur);\n    \n    res.status(201).json({\n      message: 'Inscription réussie',\n      token,\n      utilisateur\n    });\n  } catch (erreur) {\n    if (erreur.name === 'ValidationError') {\n      const messages = Object.values(erreur.errors).map(e =\u003e e.message);\n      return res.status(400).json({ erreurs: messages });\n    }\n    res.status(500).json({ erreur: 'Erreur lors de l\\'inscription' });\n  }\n});\n\n// POST /api/auth/connexion\nrouter.post('/connexion', async (req, res) =\u003e {\n  try {\n    const { email, motDePasse } = req.body;\n    \n    // Vérification des champs\n    if (!email || !motDePasse) {\n      return res.status(400).json({ erreur: 'Email et mot de passe requis' });\n    }\n    \n    // Recherche de l'utilisateur\n    const utilisateur = await Utilisateur.findOne({ email });\n    if (!utilisateur) {\n      return res.status(401).json({ erreur: 'Identifiants incorrects' });\n    }\n    \n    // Vérification du mot de passe\n    const motDePasseValide = await utilisateur.verifierMotDePasse(motDePasse);\n    if (!motDePasseValide) {\n      return res.status(401).json({ erreur: 'Identifiants incorrects' });\n    }\n    \n    // Générer le token\n    const token = genererToken(utilisateur);\n    \n    res.json({\n      message: 'Connexion réussie',\n      token,\n      utilisateur\n    });\n  } catch (erreur) {\n    res.status(500).json({ erreur: 'Erreur lors de la connexion' });\n  }\n});\n\nmodule.exports = router;\n```\n\n```javascript\n// ===== fichier : src/middleware/auth.js =====\n\nconst jwt = require('jsonwebtoken');\nconst JWT_SECRET = process.env.JWT_SECRET || 'ma_cle_secrete_tres_longue_et_complexe';\n\n// Middleware de vérification du token\nfunction authentifier(req, res, next) {\n  // Le token est envoyé dans l'en-tête Authorization sous la forme \"Bearer \u003ctoken\u003e\"\n  const authHeader = req.headers.authorization;\n  \n  if (!authHeader || !authHeader.startsWith('Bearer ')) {\n    return res.status(401).json({ erreur: 'Token d\\'authentification manquant' });\n  }\n  \n  const token = authHeader.split(' ')[1];\n  \n  try {\n    const decoded = jwt.verify(token, JWT_SECRET);\n    req.utilisateur = decoded; // Ajoute les infos utilisateur à la requête\n    next();\n  } catch (erreur) {\n    if (erreur.name === 'TokenExpiredError') {\n      return res.status(401).json({ erreur: 'Token expiré' });\n    }\n    return res.status(403).json({ erreur: 'Token invalide' });\n  }\n}\n\n// Middleware d'autorisation par rôle\nfunction autoriser(...rolesAutorises) {\n  return (req, res, next) =\u003e {\n    if (!req.utilisateur) {\n      return res.status(401).json({ erreur: 'Non authentifié' });\n    }\n    \n    if (!rolesAutorises.includes(req.utilisateur.role)) {\n      return res.status(403).json({ \n        erreur: 'Accès refusé. Rôle requis : ' + rolesAutorises.join(' ou ')\n      });\n    }\n    \n    next();\n  };\n}\n\nmodule.exports = { authentifier, autoriser };\n```\n\n```javascript\n// ===== Utilisation dans les routes protégées =====\n\nconst { authentifier, autoriser } = require('../middleware/auth');\n\n// Route accessible uniquement aux utilisateurs authentifiés\nrouter.get('/profil', authentifier, (req, res) =\u003e {\n  res.json({ utilisateur: req.utilisateur });\n});\n\n// Route accessible uniquement aux administrateurs\nrouter.delete('/etudiants/:id', authentifier, autoriser('admin'), async (req, res) =\u003e {\n  // Seul un admin peut supprimer un étudiant\n});\n\n// Route accessible aux enseignants et administrateurs\nrouter.post('/notes', authentifier, autoriser('enseignant', 'admin'), async (req, res) =\u003e {\n  // Seuls les enseignants et les admins peuvent créer des notes\n});\n```\n\n### 13.2 Variables d'environnement\n\nLes informations sensibles (clés secrètes, mots de passe de base de données) ne doivent jamais être écrites en dur dans le code. On utilise des variables d'environnement.\n\n```bash\nnpm install dotenv\n```\n\n```env\n# ===== fichier : .env (à NE JAMAIS committer dans Git) =====\nNODE_ENV=development\nPORT=3000\nMONGODB_URI=mongodb://localhost:27017/universite\nJWT_SECRET=une_tres_longue_chaine_secrete_aleatoire_42xyz\nJWT_EXPIRE=24h\n```\n\n```javascript\n// Au début de server.js\nrequire('dotenv').config();\n\nconst PORT = process.env.PORT || 3000;\nconst MONGODB_URI = process.env.MONGODB_URI;\nconst JWT_SECRET = process.env.JWT_SECRET;\n```\n\n```gitignore\n# ===== fichier : .gitignore =====\nnode_modules/\n.env\n```\n\n---\n\n## Chapitre 14 : Les frameworks backend avancés\n\n### 14.1 NestJS — L'architecture Angular côté serveur\n\nNestJS est un framework Node.js qui emprunte l'architecture d'Angular (modules, décorateurs, injection de dépendances, TypeScript natif). Il est idéal pour les applications d'entreprise qui nécessitent une structure rigoureuse.\n\n```typescript\n// ===== Exemple de contrôleur NestJS =====\n\nimport { Controller, Get, Post, Body, Param, HttpCode } from '@nestjs/common';\nimport { EtudiantService } from './etudiant.service';\nimport { CreateEtudiantDto } from './dto/create-etudiant.dto';\n\n@Controller('etudiants')   // Toutes les routes commenceront par /etudiants\nexport class EtudiantController {\n  \n  // Injection de dépendances automatique\n  constructor(private readonly etudiantService: EtudiantService) {}\n\n  @Get()\n  async findAll() {\n    return this.etudiantService.findAll();\n  }\n\n  @Get(':id')\n  async findOne(@Param('id') id: string) {\n    return this.etudiantService.findOne(id);\n  }\n\n  @Post()\n  @HttpCode(201)\n  async create(@Body() createEtudiantDto: CreateEtudiantDto) {\n    return this.etudiantService.create(createEtudiantDto);\n  }\n}\n```\n\n### 14.2 Fastify — La performance avant tout\n\nFastify se distingue par ses performances exceptionnelles (jusqu'à deux fois plus rapide qu'Express dans certains benchmarks) et son système de validation par schéma JSON intégré.\n\n```javascript\nconst fastify = require('fastify')({ logger: true });\n\n// Schéma de validation intégré (JSON Schema)\nconst schemaEtudiant = {\n  type: 'object',\n  required: ['nom', 'filiere', 'note'],\n  properties: {\n    nom: { type: 'string', minLength: 2 },\n    filiere: { type: 'string', enum: ['Informatique', 'Mathématiques', 'Physique'] },\n    note: { type: 'number', minimum: 0, maximum: 20 }\n  }\n};\n\nfastify.post('/api/etudiants', {\n  schema: {\n    body: schemaEtudiant,\n    response: {\n      201: schemaEtudiant\n    }\n  }\n}, async (request, reply) =\u003e {\n  const etudiant = await creerEtudiant(request.body);\n  reply.code(201).send(etudiant);\n});\n\nfastify.listen({ port: 3000 });\n```\n\n---\n\n# Partie III — Fullstack et écosystème\n\n---\n\n## Chapitre 15 : Next.js et Nuxt.js — Le rendu côté serveur\n\n### 15.1 Le problème des SPA pures\n\nLes SPA classiques ont deux limitations. Premièrement, le **SEO** (référencement) : les moteurs de recherche ont du mal à indexer le contenu généré dynamiquement par JavaScript. Deuxièmement, le **temps de chargement initial** : le navigateur doit télécharger tout le JavaScript avant d'afficher quoi que ce soit.\n\n### 15.2 Les stratégies de rendu\n\nLes frameworks fullstack comme Next.js et Nuxt.js proposent plusieurs stratégies de rendu.\n\n**CSR (Client-Side Rendering)** — Le rendu classique des SPA. Le HTML est vide, tout est rendu par JavaScript dans le navigateur.\n\n**SSR (Server-Side Rendering)** — Le serveur génère le HTML complet à chaque requête. Le navigateur reçoit une page prête à afficher, puis JavaScript prend le relais pour la rendre interactive (« hydratation »).\n\n**SSG (Static Site Generation)** — Les pages sont générées au moment du build (compilation). Idéal pour les contenus qui ne changent pas souvent (blogs, documentation).\n\n**ISR (Incremental Static Regeneration)** — Combinaison de SSG et SSR : les pages sont générées statiquement mais se régénèrent automatiquement à intervalles définis.\n\n### 15.3 Next.js — Le framework React fullstack\n\nNext.js, développé par Vercel, est devenu le framework React le plus populaire. Depuis la version 13, il utilise l'**App Router** avec les React Server Components.\n\n```bash\nnpx create-next-app@latest mon-app-next\ncd mon-app-next\nnpm run dev\n```\n\n```\nmon-app-next/\n├── app/                     # Dossier principal (App Router)\n│   ├── layout.js            # Layout racine (commun à toutes les pages)\n│   ├── page.js              # Page d'accueil (route /)\n│   ├── etudiants/\n│   │   ├── page.js          # Route /etudiants\n│   │   └── [id]/\n│   │       └── page.js      # Route dynamique /etudiants/:id\n│   └── api/\n│       └── etudiants/\n│           └── route.js     # API Route /api/etudiants\n├── public/                  # Fichiers statiques\n└── package.json\n```\n\n```jsx\n// ===== fichier : app/page.js — Page d'accueil (Server Component par défaut) =====\n\n// Les Server Components s'exécutent sur le serveur — pas de useState, pas de useEffect\n// Mais ils peuvent directement accéder aux données (BD, fichiers, API)\nexport default async function PageAccueil() {\n  return (\n    \u003cmain\u003e\n      \u003ch1\u003eUniversité de Mahajanga — Portail Étudiant\u003c/h1\u003e\n      \u003cp\u003eBienvenue sur le système de gestion universitaire.\u003c/p\u003e\n    \u003c/main\u003e\n  );\n}\n```\n\n```jsx\n// ===== fichier : app/etudiants/page.js — Liste des étudiants (SSR) =====\n\n// Fonction asynchrone → les données sont récupérées côté serveur\nasync function getEtudiants() {\n  const res = await fetch('http://localhost:3000/api/etudiants', {\n    cache: 'no-store'  // Force le re-fetch à chaque requête (SSR)\n    // Alternatives :\n    // cache: 'force-cache'                → SSG (données statiques)\n    // next: { revalidate: 60 }            → ISR (régénération toutes les 60 secondes)\n  });\n  \n  if (!res.ok) throw new Error('Erreur de chargement');\n  return res.json();\n}\n\nexport default async function PageEtudiants() {\n  const { etudiants } = await getEtudiants();\n  \n  return (\n    \u003cdiv\u003e\n      \u003ch1\u003eListe des étudiants\u003c/h1\u003e\n      \u003cul\u003e\n        {etudiants.map(e =\u003e (\n          \u003cli key={e.id}\u003e\n            \u003ca href={`/etudiants/${e.id}`}\u003e{e.nom}\u003c/a\u003e — {e.filiere} — {e.note}/20\n          \u003c/li\u003e\n        ))}\n      \u003c/ul\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n```jsx\n// ===== fichier : app/etudiants/[id]/page.js — Page de détail dynamique =====\n\nasync function getEtudiant(id) {\n  const res = await fetch(`http://localhost:3000/api/etudiants/${id}`);\n  if (!res.ok) throw new Error('Étudiant non trouvé');\n  return res.json();\n}\n\nexport default async function PageDetailEtudiant({ params }) {\n  const etudiant = await getEtudiant(params.id);\n  \n  return (\n    \u003cdiv\u003e\n      \u003ch1\u003e{etudiant.nom}\u003c/h1\u003e\n      \u003cp\u003eFilière : {etudiant.filiere}\u003c/p\u003e\n      \u003cp\u003eNote : {etudiant.note}/20\u003c/p\u003e\n      \u003cp\u003eStatut : {etudiant.note \u003e= 10 ? 'Admis' : 'Ajourné'}\u003c/p\u003e\n      \u003ca href=\"/etudiants\"\u003e← Retour à la liste\u003c/a\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n```javascript\n// ===== fichier : app/api/etudiants/route.js — API Route Next.js =====\n\nimport { NextResponse } from 'next/server';\n\n// Données simulées (en production → base de données)\nconst etudiants = [\n  { id: '1', nom: 'Alice Rakoto', filiere: 'Informatique', note: 16 },\n  { id: '2', nom: 'Bob Rabe', filiere: 'Mathématiques', note: 12 },\n  { id: '3', nom: 'Charlie Andria', filiere: 'Informatique', note: 14 },\n];\n\n// GET /api/etudiants\nexport async function GET() {\n  return NextResponse.json({ etudiants });\n}\n\n// POST /api/etudiants\nexport async function POST(request) {\n  const body = await request.json();\n  \n  const nouvelEtudiant = {\n    id: String(Date.now()),\n    ...body\n  };\n  \n  etudiants.push(nouvelEtudiant);\n  \n  return NextResponse.json(nouvelEtudiant, { status: 201 });\n}\n```\n\n### 15.4 Nuxt.js — Le framework Vue.js fullstack\n\nNuxt.js est l'équivalent de Next.js pour Vue.js. Il offre les mêmes fonctionnalités (SSR, SSG, routage automatique) avec la syntaxe et la philosophie Vue.\n\n```bash\nnpx nuxi@latest init mon-app-nuxt\ncd mon-app-nuxt\nnpm install\nnpm run dev\n```\n\n```vue\n\u003c!-- ===== fichier : pages/etudiants.vue — Nuxt génère automatiquement la route =====  --\u003e\n\n\u003ctemplate\u003e\n  \u003cdiv\u003e\n    \u003ch1\u003eListe des étudiants\u003c/h1\u003e\n    \n    \u003cdiv v-if=\"pending\"\u003eChargement en cours...\u003c/div\u003e\n    \u003cdiv v-else-if=\"error\"\u003eErreur : {{ error.message }}\u003c/div\u003e\n    \u003cul v-else\u003e\n      \u003cli v-for=\"e in etudiants\" :key=\"e.id\"\u003e\n        \u003cNuxtLink :to=\"`/etudiants/${e.id}`\"\u003e{{ e.nom }}\u003c/NuxtLink\u003e\n        — {{ e.filiere }} — {{ e.note }}/20\n      \u003c/li\u003e\n    \u003c/ul\u003e\n  \u003c/div\u003e\n\u003c/template\u003e\n\n\u003cscript setup\u003e\n// useFetch est un composable Nuxt qui gère le chargement des données\n// ","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgasycoder%2Fcours-framework-js-isstm-l3","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgasycoder%2Fcours-framework-js-isstm-l3","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgasycoder%2Fcours-framework-js-isstm-l3/lists"}