skilling.maSkill. Scale. Soar.
Partie 1 — Introduire le Cloud Native Cloud IaaS · PaaS · SaaS
⏱ 12 min

Définir le Cloud

Comprendre le cloud computing — définition NIST, 5 caractéristiques essentielles, fournisseurs majeurs, modèles de déploiement (public, privé, hybride) et modèles de services (IaaS, PaaS, SaaS).

ℹ Prérequis
  • Serveur : ordinateur puissant qui stocke des données et exécute des programmes accessibles via un réseau.
  • Hébergement local (on-premise) : installation et gestion des serveurs dans les locaux d'une entreprise.
  • Ressource partagée : CPU, mémoire ou stockage utilisés simultanément par plusieurs utilisateurs.
✓ Ce que vous allez apprendre
  • Définition officielle du cloud (NIST) et ses 5 caractéristiques
  • AWS, Azure, GCP — parts de marché et points forts
  • Cloud public, privé et hybride — différences et cas d'usage
  • IaaS, PaaS, SaaS — qui gère quoi ?

1
Définition du cloud et ses 5 caractéristiques
NIST · Pay-as-you-go · Élasticité
ℹ Définition officielle — NIST

Le cloud computing est un modèle permettant un accès réseau omniprésent, pratique et à la demande à un ensemble partagé de ressources informatiques configurables (réseaux, serveurs, stockage, applications et services) qui peuvent être rapidement provisionnées et libérées avec un effort de gestion minimal.

En termes simples : le cloud, c'est utiliser des ressources informatiques hébergées sur Internet, chez un fournisseur spécialisé, plutôt que de les installer soi-même. Vous payez uniquement ce que vous consommez — comme l'eau ou l'électricité.

Pour écouter de la musique, deux approches existent : acheter des milliers de CD et gérer soi-même le matériel, ou utiliser Spotify — la musique est sur leurs serveurs, accessible partout, abonnement mensuel. Le cloud applique cette même logique aux ressources informatiques d'une entreprise.

CaractéristiqueExplicationExemple concret
Libre-service à la demandeProvisionner des ressources automatiquement, sans intervention humaine du fournisseurCréer un serveur sur Azure en 2 minutes depuis son navigateur
Accès réseau largeRessources accessibles depuis n'importe quel appareil standardAccéder à son application depuis un téléphone à Fès ou un PC à Rabat
Mise en communRessources physiques partagées entre plusieurs clients de façon transparente10 entreprises marocaines partagent le même datacenter Microsoft à Amsterdam
Élasticité rapideRessources augmentées ou réduites rapidement selon les besoinsAvito.ma double sa capacité serveur en 5 minutes pendant l'Aïd
Service mesuréConsommation mesurée et facturée avec précision (pay-as-you-go)Inwi paie exactement ce qu'il consomme, à l'heure

Avantages pour une entreprise marocaine

  • Réduction des coûts : pas d'investissement initial en matériel. Une startup à Casablanca peut démarrer pour quelques dollars par mois.
  • Scalabilité : augmenter les ressources en quelques clics lors des pics de trafic — Ramadan, Black Friday, rentrée scolaire.
  • Disponibilité mondiale : l'application est accessible 24h/24, 7j/7, depuis n'importe quel coin du Maroc ou du monde.
  • Sécurité renforcée : les grands fournisseurs investissent des milliards en sécurité — un niveau qu'aucune PME ne peut se permettre seule.
  • Collaboration facilitée : plusieurs développeurs à Rabat et Casablanca peuvent travailler simultanément sur le même projet.

2
Les grands fournisseurs cloud
AWS · Azure · GCP · Contexte marocain
FournisseurNom completParts de marché 2024Points forts
AWSAmazon Web Services~32%Le plus ancien, 200+ services, présence mondiale maximale
AzureMicrosoft Azure~23%Intégration parfaite avec l'écosystème Microsoft (Office 365, Active Directory)
GCPGoogle Cloud Platform~12%Excellence en Big Data, Machine Learning et Kubernetes (inventé par Google)
OVHcloudOVH~2%Fournisseur européen, populaire au Maghreb pour sa conformité RGPD
ℹ Contexte marocain

Microsoft Azure est particulièrement présent au Maroc avec de nombreux partenariats avec des entreprises et administrations. Maroc Telecom, BMCE Bank et OCP Group utilisent des services cloud pour leurs applications critiques. Pour un développeur marocain, Azure est la connaissance la plus demandée sur le marché de l'emploi local — c'est ce que vous apprendrez dans ce module.


3
Cloud public, privé et hybride
Modèles de déploiement · Avantages · Cas d'usage

Cloud public

Infrastructure détenue et exploitée par un fournisseur tiers, partagée entre plusieurs clients via Internet. Analogie : un appartement en location dans une résidence — vous partagez le bâtiment (serveurs physiques) avec d'autres locataires, mais votre espace virtuel reste privé et isolé.

Cloud privé

Infrastructure dédiée exclusivement à une seule organisation, hébergée dans ses locaux ou chez un fournisseur tiers, mais jamais partagée. Analogie : posséder une villa — contrôle total, mais vous gérez tout : réparations, sécurité, entretien.

Exemple : Bank Al-Maghrib utilise un cloud privé pour les données financières sensibles du pays. Ces données ne peuvent pas être stockées sur des serveurs partagés pour des raisons de sécurité nationale et de conformité à la loi 09-08.

Cloud hybride

Combinaison de cloud public et privé, interconnectés pour partager données et applications. Analogie : vivre dans sa villa (cloud privé) mais louer des salles dans un hôtel voisin (cloud public) lors des grandes occasions.

Cas d'usage parfait — Maroc Telecom : les données personnelles des 50 millions de clients sont dans le datacenter privé à Rabat (loi 09-08), tandis que l'application mobile My Inwi tourne sur Azure pour absorber les pics du Ramadan. Les deux environnements communiquent via VPN sécurisé.

CritèreCloud publicCloud privéCloud hybride
Coût initialTrès faibleTrès élevéModéré
Sécurité des donnéesPartagéeMaximaleAdaptée par couche
ScalabilitéIllimitéeLimitée au matérielFlexible
ContrôleFaibleTotalPartiel
Exemple marocainAvito.ma (AWS)Bank Al-MaghribMaroc Telecom

4
IaaS, PaaS, SaaS — les modèles de services
Qui gère quoi · Tableau comparatif · Exemples Azure

IaaS — Infrastructure as a Service

Le fournisseur met à disposition des ressources d'infrastructure virtualisées : serveurs virtuels (VM), stockage, réseau, pare-feu. Le client gère tout le reste : OS, middleware, application, données. Métaphore : louer une cuisine professionnelle vide — le propriétaire fournit le matériel, vous faites tout le travail.

Exemple Azure : Azure Virtual Machines. L'équipe DevOps de OCP Group installe et configure elle-même son environnement Node.js sur une VM Ubuntu.

PaaS — Platform as a Service

Le fournisseur fournit non seulement l'infrastructure, mais aussi l'OS, le runtime et les outils de développement. Le client se concentre uniquement sur son code et ses données. Métaphore : une cuisine déjà équipée avec un chef de base — vous apportez vos recettes (le code), tout le reste est géré.

Exemple Azure : Azure App Service. Vous poussez votre code Node.js sur Git, Azure gère Node.js, SSL, le load balancer et le déploiement. C'est exactement ce que vous utiliserez dans ce module.

SaaS — Software as a Service

Le fournisseur livre une application complète via Internet. L'utilisateur n'installe, configure ou maintient rien. Métaphore : aller au restaurant — vous venez, mangez, payez. Zéro gestion.

Exemples utilisés quotidiennement au Maroc : Microsoft 365 (Word, Excel, Teams), Gmail, Salesforce (CRM de Maroc Telecom), Odoo (ERP de nombreuses PME marocaines).

⚠ Confusion fréquente — PaaS vs SaaS

SaaS = vous utilisez une application déjà faite (comme un client final).
PaaS = vous déployez votre propre application (comme un développeur).
Un développeur utilise le PaaS pour déployer son application. Un utilisateur final consomme cette même application en mode SaaS.

CoucheOn-PremiseIaaSPaaSSaaS
ApplicationClientClientClientFournisseur
DonnéesClientClientClientFournisseur
Runtime (Node.js...)ClientClientFournisseurFournisseur
Système d'exploitationClientClientFournisseurFournisseur
Serveurs physiquesClientFournisseurFournisseurFournisseur
Exemple AzureDatacenter propreVirtual MachinesApp ServiceMicrosoft 365

✓
Vérification des acquis
5 questions — corrigé intégré
Quelle caractéristique du cloud permet à Avito.ma de doubler sa capacité serveur en 5 minutes pendant l'Aïd ?
Dans le modèle PaaS, qu'est-ce que le développeur doit gérer ?
Bank Al-Maghrib stocke les données financières sensibles du pays dans son propre datacenter à Rabat. Quel modèle de déploiement utilise-t-elle ?
Vous accédez à Microsoft Word depuis votre navigateur sans rien installer. Quel modèle de service utilisez-vous ?
Une entreprise marocaine veut stocker ses données clients en interne (conformité loi 09-08), mais utiliser Azure pour son site web public. Quel modèle choisit-elle ?
Partie 1 — Introduire le Cloud Native Cloud Native Microservices · Docker · CI/CD
⏱ 14 min

L'approche Cloud Native

Comprendre pourquoi une application qui tourne dans le cloud n'est pas forcément Cloud Native — architecture monolithique, définition CNCF, 4 piliers, conteneurs vs VMs, CI/CD et les 12 facteurs.

✓ Ce que vous allez apprendre
  • Limites de l'architecture monolithique traditionnelle
  • Définition officielle du Cloud Native (CNCF)
  • Les 4 piliers : microservices, conteneurs, CI/CD, DevOps
  • Conteneurs vs machines virtuelles — différences clés
  • Les 12 facteurs d'une application Cloud Native

1
L'architecture monolithique — fonctionnement et limites
Monolithe · Problèmes concrets · Quand migrer
ℹ Définition — Architecture monolithique

Une application monolithique est une application dans laquelle tous les composants fonctionnels (interface, logique métier, authentification, notifications, base de données...) sont développés, déployés et exécutés comme une seule et unique unité indivisible.

Imaginons l'application de gestion des abonnements d'un opérateur télécom avec 3 millions d'utilisateurs. Elle contient dans un seul programme : module Authentification, module Facturation, module SMS/Notifications, module Portail Client, module Gestion des Offres, module Support. Tout dans UN SEUL programme, déployé sur UN SEUL serveur.

ProblèmeCe qui se passe concrètementImpact
Déploiement tout-ou-rienCorriger un bug dans le module SMS oblige à redéployer TOUTE l'application — 45 min d'arrêt complet3 millions d'utilisateurs sans service à chaque mise à jour
Scalabilité rigideLe module Facturation est très sollicité en fin de mois. Impossible de scaler que lui : on duplique TOUT le monolitheCoûts énormes pour scaler des modules inutilement
Effet domino des bugsUn bug dans le module SMS provoque une erreur mémoire qui fait planter toute l'applicationUn problème mineur devient une panne totale
Technologies figéesLe monolithe est en Java 8. Impossible d'utiliser Node.js pour le module temps réel sans tout réécrireInnovation technologique bloquée
Collaboration difficile20 développeurs sur le même code — conflits de merge permanentsProductivité réduite, risque d'erreurs élevé
⚠ Le monolithe n'est pas toujours mauvais

Pour une petite application avec peu d'utilisateurs et une équipe réduite, le monolithe est parfaitement adapté — il est plus simple à développer, tester et déployer. Le monolithe devient problématique quand l'application grossit et que les exigences de scalabilité et disponibilité augmentent.


2
Définition officielle du Cloud Native (CNCF)
CNCF · Scalable · Résilient · Observable
ℹ Définition officielle — CNCF

Les technologies cloud natives permettent aux organisations de construire et d'exécuter des applications scalables dans des environnements modernes et dynamiques. Les conteneurs, les microservices, l'infrastructure immuable et les APIs déclaratives illustrent cette approche. Ces techniques permettent des systèmes faiblement couplés, résilients, gérables et observables.

Une application Cloud Native n'est pas simplement une application qui tourne dans le cloud. C'est une application conçue dès le départ pour exploiter pleinement les avantages du cloud.

Terme techniqueTraduction simple
ScalableCapable de monter en charge rapidement — passer de 10k à 300k visiteurs en 5 minutes
Faiblement coupléLes composants sont indépendants — le module panier peut tomber sans affecter la recherche produit
RésilientCapable de se remettre automatiquement d'une panne — si le paiement est surchargé, l'application continue avec un message d'attente
ObservableOn peut voir en temps réel ce qui se passe — tableau de bord avec requêtes, erreurs, temps de réponse par service
ImmuableL'infrastructure ne se modifie pas, elle se remplace — on déploie une nouvelle version et on supprime l'ancienne

3
Les 4 piliers du Cloud Native
Microservices · Conteneurs · CI/CD · DevOps
PilierDéfinition courteDans ce module
1. MicroservicesDécouper l'application en petits services autonomes, chacun responsable d'une seule fonctionPartie 3 — créer une application microservices Node.js (auth, produit, commande)
2. ConteneursEmballer chaque service avec tout ce dont il a besoin, de manière portable et isoléePartie 4 — Docker : images, conteneurs, Docker Compose
3. CI/CDAutomatiser les tests et le déploiement pour livrer fréquemment et en sécuritéPartie 5 — déploiement continu sur Azure App Service depuis Docker Hub
4. DevOpsCulture et pratiques qui rapprochent développement et opérations pour livrer plus viteIntégré tout au long du module : pipelines, monitoring, logs Azure

Microservices — principe clé

Un microservice se concentre sur une seule responsabilité fonctionnelle, possède sa propre base de données, et peut être déployé, mis à jour et scalé indépendamment des autres services. Résultats concrets :

  • Une mise à jour du service SMS n'affecte pas le portail client — zéro interruption globale.
  • En fin de mois, on scale uniquement le service Facturation × 10. Économies massives.
  • Si le service SMS tombe, les clients peuvent toujours consulter leur facture et se connecter.
  • Chaque équipe (2-3 développeurs) est propriétaire de son service — pas de conflits de code.
⚠ Confusion fréquente — Microservice ≠ API REST

Un microservice n'est pas simplement une API REST. La différence fondamentale :
API REST = interface de communication (comment on parle au service).
Microservice = service autonome complet avec sa propre logique, sa propre BDD et son propre cycle de vie.
Un microservice expose une API REST, mais il est bien plus qu'une API.


4
Conteneurs vs Machines Virtuelles
Docker · Isolation · Portabilité · Performance

Avant les conteneurs, le problème classique du développement : un développeur code sur Ubuntu 22 avec Node.js 18, son collègue déploie sur Ubuntu 20 avec Node.js 16 — l'application plante sur le serveur. Avec les conteneurs, ce problème disparaît.

ℹ Définition — Conteneur Docker

Un conteneur est un paquet logiciel léger et autonome qui contient tout ce dont une application a besoin pour fonctionner : le code, le runtime (Node.js 18), les dépendances npm, les variables de configuration et les fichiers système. Ce paquet s'exécute de manière identique sur n'importe quelle machine — PC de développement ou serveur Azure en Europe.

Analogie : le conteneur de transport maritime. Avant les années 1950, charger un bateau prenait des semaines. Le conteneur standardisé a révolutionné le transport mondial. Docker a fait la même révolution pour le logiciel.

CritèreMachine Virtuelle (VM)Conteneur Docker
TaillePlusieurs Go (OS complet inclus)Quelques Mo à centaines de Mo
Démarrage2 à 5 minutesMoins d'une seconde
IsolationComplète (OS dédié)Processus isolé, OS partagé
PortabilitéLourde (fichier .vmdk, .vhd)Légère (image sur Docker Hub)
Densité serveur5 à 20 VMs par serveur physiqueCentaines de conteneurs par serveur

5
Les 12 facteurs d'une application Cloud Native
Twelve-Factor App · Heroku · Bonnes pratiques

The Twelve-Factor App est une méthodologie publiée par les ingénieurs d'Heroku. Elle définit 12 bonnes pratiques que toute application Cloud Native doit respecter. Ces facteurs sont devenus la référence mondiale — vous les appliquerez tout au long de ce module.

#FacteurPrincipeApplication dans ce module
ICodebaseUn seul dépôt Git par serviceChaque microservice a son propre repo GitHub
IIDependenciesDéclarer explicitement toutes les dépendancespackage.json liste exactement les packages requis
IIIConfigConfiguration dans les variables d'environnement, jamais dans le codeSecrets JWT et URLs MongoDB dans .env, jamais en dur dans index.js
VIProcessesApplication sans état (stateless)Toutes les APIs REST de ce module sont stateless — chaque requête est indépendante
VIIPort BindingL'application expose son propre portapp.listen(3000) — Express écoute sur le port 3000
XDev/Prod ParityEnvironnements de développement et production similairesDocker garantit que ce qui tourne sur votre PC tourne identiquement sur Azure
XILogsÉcrire les logs sur stdoutconsole.log() Node.js → Azure collecte automatiquement les logs
✕ Règle absolue — Facteur III (Config)

Ne jamais mettre un mot de passe, une clé API ou une URL de base de données directement dans le code source. Utilisez toujours des variables d'environnement (.env). C'est une règle de sécurité non négociable — enfreindre cette règle expose vos credentials sur Git et peut compromettre toute votre infrastructure.


6
Traditionnel vs Cloud Native — tableau comparatif
Monolithe vs Microservices · Lift & Shift
CritèreApplication TraditionnelleApplication Cloud Native
ArchitectureUn seul bloc indivisibleEnsemble de microservices indépendants
DéploiementTout ou rien, 30–60 min d'arrêtService par service, zéro interruption
ScalabilitéL'application entière, coût élevéService par service, coût optimisé
PanneUn bug peut tout planterIsolation : une panne reste locale
TechnologieUne seule techno pour toutLiberté par service (polyglotte)
InfrastructureServeurs fixes, configurés manuellementConteneurs, infrastructure immuable
Temps de livraisonSemaines ou moisHeures ou jours
Exemple marocainSI CNSS avant transformationPlateforme Damancom après transformation
⚠ Le piège du "Lift and Shift"

Migrer une application monolithique telle quelle vers le cloud (sans la transformer) s'appelle le Lift and Shift. L'application tourne bien dans le cloud, mais ne tire aucun avantage réel du cloud : elle reste lente à déployer, difficile à scaler et fragile face aux pannes. C'est une erreur très fréquente en entreprise.


✓
Exercice — Cloud Native ou pas ?
Classement · Justification

Pour chaque description, déterminez si l'application est Cloud Native ou non, et pourquoi.

Une application de paie dont toute la logique (RH, paie, congés) est dans un seul programme Java. Un déploiement mensuel nécessite 2h d'arrêt système.
Une plateforme e-commerce décomposée en services indépendants (catalogue, panier, paiement, livraison), chacun dans son propre conteneur Docker. Une mise à jour du service paiement se déploie en 5 min sans toucher aux autres.
Une API Node.js uploadée directement sur un VPS. Les mots de passe de la base de données sont écrits en dur dans index.js. Pour scaler, il faut louer manuellement un deuxième VPS.
Partie 2 — APIs REST avec Node.js & Express Node.js npm · CommonJS · Event Loop
⏱ 16 min

Introduction à Node.js

Comprendre Node.js, son moteur V8, le modèle non-bloquant (event loop), npm, les modules CommonJS — puis créer un premier serveur HTTP natif avant de découvrir pourquoi Express est indispensable.

ℹ Prérequis
  • API REST : interface de communication entre deux applications via HTTP, utilisant des URLs et des méthodes standardisées.
  • Méthodes HTTP : GET (lire), POST (créer), PUT (modifier), DELETE (supprimer).
  • JSON : format d'échange de données léger — utilisé comme langage commun entre client et serveur.
  • Terminal : interface textuelle pour exécuter des commandes sur le système.
✓ Ce que vous allez apprendre
  • Ce qu'est Node.js et pourquoi il est idéal pour les APIs Cloud Native
  • Le moteur V8 et le modèle non-bloquant — la clé des performances Node.js
  • npm : initialiser un projet, installer et gérer les dépendances
  • Les modules CommonJS : require() et module.exports
  • Créer un premier serveur HTTP natif
  • Pourquoi Express simplifie radicalement le développement d'APIs

1
Qu'est-ce que Node.js ?
Historique · V8 · Positionnement · Comparaison
ℹ Définition — Node.js

Node.js est un environnement d'exécution JavaScript côté serveur, open source et multiplateforme, construit sur le moteur JavaScript V8 de Google Chrome. Il permet d'exécuter du code JavaScript en dehors d'un navigateur web, directement sur un serveur.

Avant 2009, JavaScript ne s'exécutait que dans le navigateur. Node.js a révolutionné le développement en permettant d'utiliser un seul langage (JavaScript) pour le frontend et le backend.

Historique

  • 2008 : Google crée le moteur V8 pour Chrome — compile le JavaScript en code machine ultra-rapide.
  • 2009 : Ryan Dahl utilise V8 pour créer Node.js. Première exécution de JavaScript côté serveur.
  • 2010 : npm (Node Package Manager) voit le jour — des milliers de modules partagés gratuitement.
  • 2013 : Netflix, LinkedIn et PayPal migrent leurs backends vers Node.js. Performances multipliées par 10.
  • Aujourd'hui : Node.js propulse les backends de Netflix, Uber, LinkedIn et des centaines de startups africaines.
CritèreNode.jsPHPJava Spring
LangageJavaScript (front + back identique)PHP (dédié back)Java (verbeux)
Démarrage conteneur< 1 seconde1–2 secondes5–30 secondes
Consommation mémoireTrès faible (~50 Mo)ModéréeÉlevée (JVM ~200 Mo+)
Scalabilité Cloud NativeIdéal : léger, stateless, rapideAcceptableTrès bien mais coûteux
Packages disponibles1,5 million+ via npm~100k via Composer~500k via Maven

2
Le modèle non-bloquant — Event Loop
Asynchrone · Callbacks · Performance

C'est le concept le plus important de Node.js. Il explique pourquoi Node.js est si performant pour les APIs REST avec des milliers de requêtes simultanées.

⚠ Le problème du modèle bloquant

Dans un modèle bloquant (PHP classique) : quand le serveur attend une réponse de la base de données, il s'arrête complètement. Si 100 requêtes arrivent simultanément, 99 attendent dans la file. Le thread est bloqué pendant toute la durée de l'opération lente.

✓ La solution Node.js — modèle non-bloquant

Node.js délègue les opérations lentes (lecture BDD, fichiers, réseau) et continue immédiatement à traiter d'autres requêtes. Quand l'opération se termine, une fonction callback est appelée avec le résultat. Un seul thread Node.js peut gérer des milliers de connexions simultanées.

javascript comparaison-blocking.js
//  Approche BLOQUANTE — ne pas utiliser en Node.js
const donnees = lirebdd();   // Le programme s'ARRÊTE et attend
                              // Pendant ce temps : 0 requête traitée
console.log(donnees);

//  Approche NON-BLOQUANTE avec callback — Node.js
lirebdd(function(donnees) {
  console.log(donnees);       // Exécuté QUAND la BDD répond
});
// Node.js continue immédiatement ici — autres requêtes traitées
javascript event-loop-demo.js
// Exercice classique — prédire l'ordre d'affichage
console.log('1 - Début');
setTimeout(function() {
  console.log('2 - setTimeout');
}, 0);  // 0 ms de délai !
console.log('3 - Fin');

// Résultat : 1, 3, 2
// Explication : Node.js exécute d'abord tout le code synchrone (1 et 3),
// puis traite les callbacks (2) — même avec un délai de 0 ms.

3
npm — le gestionnaire de paquets
package.json · node_modules · Scripts · .gitignore

npm (Node Package Manager) permet d'installer, partager et gérer des packages dans un projet Node.js. Il contient aujourd'hui plus de 1,5 million de packages gratuits. Au lieu de coder une fonction JWT de zéro (des centaines de lignes), vous installez le package jsonwebtoken en une commande.

bash terminal
# Vérifier l'installation
node --version      # Ex : v20.11.0
npm --version       # Ex : 10.2.4

# Initialiser un projet
mkdir mon-api && cd mon-api
npm init -y         # Crée package.json avec valeurs par défaut

# Installer des dépendances
npm install express                # Production → dependencies
npm install nodemon --save-dev     # Développement → devDependencies

# Après un git clone (réinstaller tout depuis package.json)
npm install
json package.json
{
  "name": "mon-api",
  "version": "1.0.0",
  "description": "API REST Node.js — skilling.ma",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",       // Production  : npm start
    "dev":   "nodemon index.js"     // Développement : npm run dev
  },
  "dependencies": {
    "express": "^4.18.2"            // Dépendance de production
  },
  "devDependencies": {
    "nodemon": "^3.0.1"             // Seulement en développement
  }
}
⚠ Ne jamais versionner node_modules/

Le dossier node_modules/ peut peser plusieurs centaines de Mo. Créer un fichier .gitignore à la racine avec le contenu node_modules/. Un simple npm install sur n'importe quelle machine recrée tout le dossier à partir de package.json.


4
Modules CommonJS — require() et module.exports
Modules locaux · Modules natifs · Modules npm

Un module est simplement un fichier JavaScript. Le système CommonJS permet de découper le code en fichiers réutilisables et de les importer les uns dans les autres. C'est le système original de Node.js — le plus répandu dans les projets existants.

Il existe 3 types de modules :

  • Modules locaux : fichiers .js du projet → require('./calculatrice')
  • Modules natifs : intégrés à Node.js, pas de npm install → require('http'), require('fs')
  • Modules npm : installés via npm → require('express')
javascript calculatrice.js
// calculatrice.js — définir et exporter des fonctions

function addition(a, b) {
  return a + b;
}

function multiplication(a, b) {
  return a * b;
}

// Sans module.exports, les fonctions sont inaccessibles depuis l'extérieur
module.exports = {
  addition:       addition,
  multiplication: multiplication
};
javascript app.js
// app.js — importer et utiliser le module
const calc = require('./calculatrice'); // './' = même dossier

console.log('5 + 3 =', calc.addition(5, 3));         // 8
console.log('4 × 3 =', calc.multiplication(4, 3));   // 12
⚠ Erreur — Cannot find module

Si Node.js affiche Error: Cannot find module './calculatrice' :

  • Le fichier calculatrice.js est-il dans le même dossier ? Vérifier avec ls (Mac/Linux) ou dir (Windows).
  • Le nom du fichier est-il exactement identique ? Node.js est sensible à la casse sur Linux.
  • Le chemin commence-t-il par ./ ? Écrire require('./calculatrice') et non require('calculatrice').
Module natifUtilitéExemple d'utilisation
httpCréer des serveurs HTTP sans Expressconst http = require('http')
fsLire et écrire des fichiersconst fs = require('fs')
pathConstruire des chemins de fichiers cross-platformconst path = require('path')
eventsCréer et gérer des événementsconst EventEmitter = require('events')
osInfos sur le système d'exploitationconst os = require('os')

5
Premier serveur HTTP avec Node.js natif
module http · createServer · listen · Codes HTTP

Le module natif http permet de créer un serveur web sans installer aucune dépendance externe. C'est la base de toute API REST — Express est construit entièrement par-dessus ce module.

javascript serveur.js
// serveur.js — Premier serveur HTTP Node.js natif (sans Express)
const http = require('http');
const PORT = 3000;

const serveur = http.createServer(function(req, res) {
  const url     = req.url;    // Ex: '/produits' ou '/'
  const methode = req.method; // Ex: 'GET', 'POST'

  console.log(`${methode} ${url}`); // Log chaque requête dans le terminal

  if (url === '/' && methode === 'GET') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({
      message: 'API Node.js opérationnelle',
      version: '1.0.0'
    }));

  } else if (url === '/produits' && methode === 'GET') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({
      produits: [
        { id: 1, nom: 'Tajine',   prix: 120 },
        { id: 2, nom: 'Couscous', prix: 85  }
      ]
    }));

  } else {
    res.writeHead(404, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({
      erreur:       'Route introuvable',
      url_demandee: url
    }));
  }
});

serveur.listen(PORT, function() {
  console.log(`Serveur démarré sur http://localhost:${PORT}`);
});
URL à testerRésultat attenduCode HTTP
http://localhost:3000/{ message: 'API Node.js opérationnelle' }200 OK
http://localhost:3000/produitsTableau des 2 produits200 OK
http://localhost:3000/clients{ erreur: 'Route introuvable' }404 Not Found

6
Pourquoi Express ? Comparaison Node natif vs Express
Routing · Middleware · Paramètres · Lisibilité

Le serveur HTTP natif fonctionne, mais pour 20 routes avec paramètres, gestion du body et authentification, le code deviendrait une montagne de if/else illisibles. Problèmes concrets :

  • Routing : un if/else par route → illisible dès 10 routes.
  • Paramètres d'URL : extraire l'id dans /produits/123 = code manuel fragile.
  • Parsing du body : lire le JSON d'un POST nécessite ~15 lignes de code natif.
  • Middleware : impossible d'ajouter logs, auth ou CORS facilement sur toutes les routes.
javascript node-natif-vs-express.js
//  Node.js natif — lourd et fragile pour 20+ routes
const http = require('http');
http.createServer(function(req, res) {
  if (req.url === '/produits' && req.method === 'GET') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ produits: [] }));
  } else if (req.url.startsWith('/produits/') && req.method === 'GET') {
    const id = req.url.split('/')[2]; // fragile — ne gère pas /produits/1/photos
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ id: id }));
  } // ... et ainsi de suite pour chaque route
}).listen(3000);

//  Express — lisible, maintenable, professionnel
const express = require('express');
const app = express();
app.use(express.json()); // Parse le JSON du body automatiquement

app.get('/produits', function(req, res) {
  res.json({ produits: [] }); // Content-Type + JSON.stringify automatiques
});

app.get('/produits/:id', function(req, res) {
  const id = req.params.id; // Extraction automatique, sûre
  res.json({ produit_id: id });
});

app.listen(3000);
FonctionnalitéNode.js natifExpress.js
Route GETif (url === '/x' && method === 'GET')app.get('/x', handler)
Paramètre :idreq.url.split('/')[2] — fragilereq.params.id — sûr et simple
Lire le body JSON~15 lignes de code manuelapp.use(express.json()) — 1 ligne
Envoyer du JSONres.writeHead(200,{...}); res.end(JSON.stringify(...))res.json({...})
Middleware globalModification de chaque routeapp.use(monMiddleware)
✓ À retenir

Express.js n'est pas un remplacement de Node.js — c'est une couche d'abstraction construite au-dessus pour simplifier la création d'APIs REST. Express propulse les APIs de Uber, Twitter, IBM et des milliers d'entreprises. Dans toutes les séances suivantes, vous utiliserez toujours Express.


✓
Débogage — trouvez les 5 erreurs
Erreurs classiques · Correction

Lisez le code ci-dessous. Identifiez les 5 erreurs avant de regarder le corrigé.

javascript buggy-server.js — 5 erreurs à trouver
const express = require('Express');       // Ligne 1
const app = express;                      // Ligne 2

app.get('/articles', function(req, res) {
  res.status(200).json({ articles: [] });
});

app.get('/articles/:id', function(req, res) {
  const id = req.body.id;                 // Ligne 9
  res.json({ id: id });
});

app.post('/articles', function(req, res) {
  const article = req.body;
  res.status(200).json({ article });      // Ligne 15
});

app.Listen(3000, function() {             // Ligne 18
  console.log('Serveur démarré');
});
ℹ Corrigé — les 5 erreurs
  • Ligne 1 : require('Express') → require('express') — Node.js est sensible à la casse, le package npm s'appelle express en minuscules.
  • Ligne 2 : const app = express → const app = express() — express est une fonction constructeur, elle doit être appelée avec ().
  • Ligne 9 : req.body.id → req.params.id — l'id est dans l'URL (/articles/:id), donc dans req.params. req.body contient le body d'un POST/PUT.
  • Ligne 15 : res.status(200) → res.status(201) — une route POST crée une ressource, le code HTTP correct est 201 Created, pas 200 OK.
  • Ligne 18 : app.Listen() → app.listen() — JavaScript est sensible à la casse, la méthode Express est listen en minuscules.
Partie 2 — APIs REST avec Node.js & Express Express.js Routing · Middleware · Callbacks
⏱ 18 min

Express.js — Fondamentaux

Maîtriser le module events, les callbacks asynchrones, l'installation d'Express, le routing complet (GET/POST/PUT/DELETE), les réponses HTTP et les codes de statut.

✓ Ce que vous allez apprendre
  • Le module events — EventEmitter, .on(), .emit(), .once()
  • Les callbacks et la convention error-first
  • Installer et configurer Express dans un projet
  • Router avec app.get(), app.post(), app.put(), app.delete()
  • Les méthodes de réponse : res.json(), res.status(), res.send()
  • Les codes HTTP essentiels pour une API REST

1
Le module events — EventEmitter
.on() · .emit() · .once() · Architecture événementielle
ℹ Définition — Événement Node.js

Un événement est une notification émise quand quelque chose se produit : une requête HTTP arrive, un fichier finit d'être lu, une connexion BDD est établie. Le module natif events fournit la classe EventEmitter avec deux opérations fondamentales :

  • .on(nomEvenement, callback) → s'abonner à un événement (écouter)
  • .emit(nomEvenement, données) → déclencher un événement (notifier)

Comprendre ce modèle est essentiel : Express, MongoDB et RabbitMQ l'utilisent tous en interne. Quand une requête HTTP arrive, Node.js émet un événement 'request' — Express s'y abonne pour gérer le routing.

javascript events-demo.js
const EventEmitter = require('events'); // Module natif — pas de npm install

const emetteur = new EventEmitter();

// Deux listeners indépendants sur le même événement
emetteur.on('nouvelle_commande', function(commande) {
  console.log('Stock mis à jour pour :', commande.produit);
});

emetteur.on('nouvelle_commande', function(commande) {
  console.log('Email de confirmation envoyé à :', commande.client);
});

// .once() s'exécute UNE SEULE fois puis se désabonne automatiquement
emetteur.once('demarrage', function() {
  console.log('Serveur initialisé — ce message n'apparaît qu'une fois');
});

emetteur.emit('demarrage');            // → affiche le message
emetteur.emit('demarrage');            // → rien (once désabonné)

emetteur.emit('nouvelle_commande', {
  client:  'Mohamed',
  produit: 'Tajine',
  prix:    120
});
// Résultat : les deux listeners s'exécutent indépendamment

2
Les callbacks — gérer l'asynchronisme
Error-first · fs.readFile · Callback Hell

Un callback est une fonction passée en argument à une autre fonction, appelée automatiquement quand une opération asynchrone se termine. C'est le mécanisme fondamental de Node.js pour ne pas bloquer le thread principal.

javascript callbacks-demo.js
const fs = require('fs');

console.log('1 - Début');

setTimeout(function() {
  console.log('3 - Après 2 secondes');
}, 2000);

console.log('2 - Fin du code synchrone');
// Ordre d'affichage : 1, 2, (2 secondes), 3

// Lecture de fichier avec callback — convention error-first
fs.readFile('data.json', 'utf8', function(erreur, contenu) {
  // PREMIER argument = toujours l'erreur (null si pas d'erreur)
  if (erreur) {
    console.error('Erreur lecture :', erreur.message);
    return; // Stopper si erreur — ne jamais continuer sans données valides
  }
  // SECOND argument = le résultat (seulement si pas d'erreur)
  const donnees = JSON.parse(contenu);
  console.log('Données lues :', donnees);
});
⚠ Convention obligatoire — Error-first callback

Tous les callbacks Node.js suivent la convention error-first : le premier argument est toujours l'erreur, le second est le résultat.

  • Correct : function(erreur, resultat) { if (erreur) { return; } ... }
  • Incorrect : function(resultat) { ... } — ne gère pas l'erreur

Une erreur non gérée peut planter tout le serveur Node.js en production.


3
Installer et configurer Express
npm install · express.json() · nodemon
bash terminal
mkdir mon-api && cd mon-api
npm init -y
npm install express
npm install nodemon --save-dev
javascript index.js — structure de base Express
const express = require('express');
const app     = express();        // Appeler express() avec () — erreur classique : oublier les ()
const PORT    = 3000;

// Middleware global : parse automatiquement le body JSON des requêtes POST/PUT
// Sans cette ligne, req.body sera undefined dans toutes les routes POST/PUT
app.use(express.json());

// Routes ici...

app.listen(PORT, function() {
  console.log(`Serveur Express : http://localhost:${PORT}`);
});

4
Routing Express — CRUD complet
app.get · app.post · app.put · app.delete · req.params · req.body
ℹ Syntaxe générale d'une route Express

app.METHODE(CHEMIN, HANDLER)
METHODE → get / post / put / delete (toujours en minuscules)
CHEMIN → '/produits', '/produits/:id'
HANDLER → function(req, res) { ... }

javascript index.js — CRUD complet avec Express
const express = require('express');
const app     = express();
const PORT    = 3000;
app.use(express.json());

// Données statiques (remplacées par MongoDB en séance 8)
const produits = [
  { id: 1, nom: 'Tajine',   prix: 120 },
  { id: 2, nom: 'Couscous', prix: 85  },
  { id: 3, nom: 'Harira',   prix: 45  }
];

// ── GET /produits — liste complète ────────────────────
app.get('/produits', function(req, res) {
  res.status(200).json({
    produits: produits,
    total: produits.length // Bonne pratique : inclure le total
  });
});

// ── GET /produits/:id — un produit par id ────────────
app.get('/produits/:id', function(req, res) {
  const id      = parseInt(req.params.id); // req.params.id est toujours une string
  const produit = produits.find(p => p.id === id);

  if (!produit) {
    return res.status(404).json({
      erreur:     'Produit introuvable',
      id_demande: id
    });
  }
  res.status(200).json({ produit });
});

// ── POST /produits — créer un produit ────────────────
app.post('/produits', function(req, res) {
  // Validation des données — toujours valider avant de traiter
  if (!req.body.nom || !req.body.prix) {
    return res.status(400).json({
      erreur: 'Les champs nom et prix sont obligatoires'
    });
  }
  res.status(201).json({ // 201 Created — pas 200 pour une création
    message: 'Produit créé avec succès',
    produit: req.body
  });
});

// ── PUT /produits/:id — modifier un produit ──────────
app.put('/produits/:id', function(req, res) {
  const id = parseInt(req.params.id);
  res.status(200).json({
    message:       `Produit ${id} modifié avec succès`,
    modifications: req.body
  });
});

// ── DELETE /produits/:id — supprimer un produit ──────
app.delete('/produits/:id', function(req, res) {
  const id = parseInt(req.params.id);
  res.status(200).json({
    message: `Produit ${id} supprimé avec succès`
  });
});

app.listen(PORT, () => console.log(`http://localhost:${PORT}`));

5
Codes HTTP et méthodes de réponse
res.json · res.status · Codes essentiels
Méthode ExpressUtilisationÉquivalent Node.js natif
res.json(objet)Envoyer du JSON — définit Content-Type: application/json automatiquementres.writeHead(200,...); res.end(JSON.stringify(...))
res.status(code)Définir le code HTTP — se chaîne avec .json()res.writeHead(code, headers)
res.send(texte)Envoyer du texte brut ou HTMLres.end(texte)
res.sendStatus(204)Envoyer uniquement un code HTTP, sans bodyres.writeHead(204); res.end()
CodeSignificationQuand l'utiliser
200 OKSuccèsGET réussi, PUT réussi, DELETE réussi
201 CreatedRessource crééePOST réussi — création d'un produit, utilisateur...
204 No ContentSuccès sans réponseDELETE réussi quand on ne renvoie rien
400 Bad RequestDonnées invalidesBody JSON mal formé, champ obligatoire absent
401 UnauthorizedAuth requiseToken JWT absent ou invalide
403 ForbiddenAccès refuséAuthentifié mais sans les droits suffisants
404 Not FoundRessource introuvableID inexistant en base
500 Server ErrorErreur serveurException non gérée dans le code

✓
Tableau de bord des requêtes
Vérifiez votre compréhension du routing Express

Pour chaque requête ci-dessous, déterminez le code HTTP que le serveur doit renvoyer et pourquoi.

RequêteSituationCode attendu
GET /produitsTableau de 3 produits retourné avec succès200 OK
GET /produits/99Aucun produit avec l'id 99 dans les données404 Not Found
POST /produits avec { nom: 'Tajine' }Le champ prix est obligatoire mais absent400 Bad Request
POST /produits avec { nom: 'Tajine', prix: 120 }Produit créé avec succès201 Created
DELETE /produits/1Produit supprimé avec succès200 OK ou 204
GET /commandesRoute non définie dans le serveur404 Not Found
Dans la route app.get('/produits/:id', ...), quelle ligne de code extrait correctement l'id numérique depuis l'URL /produits/5 ?
Pourquoi faut-il appeler app.use(express.json()) avant de définir les routes POST ?
Partie 2 — APIs REST avec Node.js & Express Routes GET JSON · fs · Postman
⏱ 15 min

Routes GET sur fichier JSON

Construire une API REST qui lit ses données depuis un fichier JSON — conventions de nommage REST, routes GET liste et par id, gestion des erreurs, organisation du projet avec Express Router, et test avec Postman.

✓ Ce que vous allez apprendre
  • Structure et règles de syntaxe d'un fichier JSON valide
  • Conventions de nommage des URLs REST (ressources, pas actions)
  • Organiser un projet Express avec express.Router()
  • Implémenter GET /equipes — liste complète avec fs.readFile
  • Implémenter GET /equipes/:id — une équipe par identifiant
  • Installer Postman et tester les 4 cas (200, 404, 400, 500)

1
Structure d'un fichier JSON valide
Règles de syntaxe · Erreurs fréquentes · Validation

Avant MongoDB, les données sont stockées dans un fichier .json. Cela permet de se concentrer sur l'API REST sans installer de base de données. La transition vers MongoDB sera minime : seule la couche d'accès aux données change.

RègleCorrect Incorrect
Guillemets doubles sur les clés{ "nom": "Wydad" }{ nom: 'Wydad' }
Guillemets doubles sur les chaînes{ "ville": "Casablanca" }{ "ville": 'Casablanca' }
Pas de virgule finale{ "a": 1, "b": 2 }{ "a": 1, "b": 2, }
Pas de commentaires{ "id": 1 }{ "id": 1 // identifiant }
Nombres sans guillemets{ "annee": 1949 }{ "annee": "1949" }
Booléens sans guillemets{ "actif": true }{ "actif": "true" }
json data/equipes.json
{
  "equipes": [
    {
      "id": 1,
      "nom": "Wydad Club Athlétique",
      "ville": "Casablanca",
      "fondation": 1937,
      "stade": "Complexe Mohammed V",
      "titres_nationaux": 22
    },
    {
      "id": 2,
      "nom": "Raja Athletic Club",
      "ville": "Casablanca",
      "fondation": 1949,
      "stade": "Complexe Mohammed V",
      "titres_nationaux": 13
    },
    {
      "id": 3,
      "nom": "FUS Rabat",
      "ville": "Rabat",
      "fondation": 1946,
      "stade": "Stade Prince Moulay Abdellah",
      "titres_nationaux": 5
    },
    {
      "id": 4,
      "nom": "Moghreb Athlétique Tétouan",
      "ville": "Tetouan",
      "fondation": 1922,
      "stade": "Stade Saniat Rmel",
      "titres_nationaux": 2
    },
    {
      "id": 5,
      "nom": "Hassania Union Sport Agadir",
      "ville": "Agadir",
      "fondation": 1946,
      "stade": "Stade Adrar",
      "titres_nationaux": 1
    }
  ]
}
ℹ Valider un fichier JSON

Dans VS Code, un soulignement rouge signale une erreur de syntaxe JSON. Pour valider en ligne : jsonlint.com. Ne jamais avancer avec un fichier JSON invalide — JSON.parse() lèvera une exception qui plantera le serveur.


2
Conventions de nommage REST
Ressources vs Actions · Pluriel · Structure des URLs
ℹ Règle fondamentale — URLs = ressources, pas actions

Dans une API REST, les URLs représentent des ressources (des objets métier), jamais des actions. La méthode HTTP exprime l'action.

  • /getEquipes, /creerEquipe, /supprimerEquipe/1
  • GET /equipes, POST /equipes, DELETE /equipes/1
MéthodeURLActionCode succès
GET/equipesRetourner toutes les équipes200 OK
GET/equipes/:idRetourner une équipe par id200 OK
POST/equipesCréer une nouvelle équipe201 Created
PUT/equipes/:idModifier une équipe200 OK
DELETE/equipes/:idSupprimer une équipe200 OK

Conventions supplémentaires

  • Pluriel : /equipes (pas /equipe), /joueurs (pas /joueur)
  • Minuscules : /equipes-nationales (pas /EquipesNationales)
  • Sous-ressources : GET /equipes/1/joueurs → liste des joueurs de l'équipe 1

3
Organisation du projet avec Express Router
express.Router() · Séparation par ressource · path.join()

Pour les projets avec plusieurs ressources, séparer les routes dans des fichiers dédiés est une bonne pratique. express.Router() crée un mini-routeur indépendant qui s'intègre ensuite dans l'application principale.

text Structure du projet
api-equipes/
├── data/
│   └── equipes.json       ← données
├── routes/
│   └── equipes.js         ← toutes les routes /equipes
├── .gitignore             ← contient : node_modules/
├── package.json
└── index.js               ← point d'entrée du serveur
javascript routes/equipes.js
const express = require('express');
const router  = express.Router(); // Mini-routeur indépendant
const fs      = require('fs');
const path    = require('path');

// path.join() construit un chemin compatible Windows et Linux
const FICHIER = path.join(__dirname, '..', 'data', 'equipes.json');

// Fonction utilitaire réutilisable dans toutes les routes
function lireEquipes(callback) {
  fs.readFile(FICHIER, 'utf8', function(erreur, contenu) {
    if (erreur) {
      callback(erreur, null);
      return;
    }
    const donnees = JSON.parse(contenu);
    callback(null, donnees.equipes);
  });
}

// ── GET /equipes — liste complète ─────────────────────
router.get('/', function(req, res) {  // Chemin '/' car préfixe /equipes défini dans index.js
  lireEquipes(function(erreur, equipes) {
    if (erreur) {
      return res.status(500).json({ erreur: 'Erreur serveur' });
    }
    res.status(200).json({
      total:   equipes.length,
      equipes: equipes
    });
  });
});

// ── GET /equipes/:id — une équipe ─────────────────────
router.get('/:id', function(req, res) {
  const idDemande = parseInt(req.params.id);

  // Validation : l'id doit être un nombre
  if (isNaN(idDemande)) {
    return res.status(400).json({ erreur: 'L'id doit être un nombre entier' });
  }

  lireEquipes(function(erreur, equipes) {
    if (erreur) {
      return res.status(500).json({ erreur: 'Erreur serveur' });
    }
    const equipe = equipes.find(e => e.id === idDemande);

    if (!equipe) {
      return res.status(404).json({
        erreur:     'Equipe introuvable',
        id_demande: idDemande
      });
    }
    res.status(200).json({ equipe });
  });
});

module.exports = router;
javascript index.js
const express = require('express');
const app     = express();
const PORT    = 3000;

app.use(express.json());

// Monter le router equipes — toutes ses routes seront préfixées par /equipes
const equipesRouter = require('./routes/equipes');
app.use('/equipes', equipesRouter);

app.get('/', function(req, res) {
  res.json({ message: 'API Equipes — opérationnelle', routes: ['GET /equipes', 'GET /equipes/:id'] });
});

app.listen(PORT, () => console.log(`http://localhost:${PORT}`));

4
Tester avec Postman
Installation · Collections · Tests des 4 cas
ℹ Pourquoi Postman ?

Le navigateur ne permet de tester que les requêtes GET. Pour tester POST, PUT et DELETE, un outil dédié est nécessaire. Postman est l'outil standard dans l'industrie — il permet d'envoyer n'importe quelle requête HTTP, d'ajouter des headers, un body JSON, et de sauvegarder les tests dans des Collections réutilisables.

URLRésultat attenduCode HTTPCas testé
GET /equipesTableau des 5 équipes + total: 5200Succès nominal
GET /equipes/3Objet équipe FUS Rabat200Id valide existant
GET /equipes/99{ erreur: 'Equipe introuvable', id_demande: 99 }404Id valide mais inexistant
GET /equipes/abc{ erreur: 'L'id doit être un nombre entier' }400Id non numérique

Tester les cas d'erreur est aussi important que tester le cas nominal. Une API professionnelle renvoie des messages d'erreur clairs et des codes HTTP corrects — pas uniquement le code 200 pour tout.

CapacitéNavigateurPostman
Tester GET
Tester POST avec body JSON
Tester PUT et DELETE
Voir le code HTTP (DevTools requis) directement
Sauvegarder les requêtes Collections
Partager les tests avec l'équipe Export JSON

✓
Complétez le code
Exercice de compréhension

Remplissez les blancs pour que la route GET /stades/:nom fonctionne correctement :

javascript routes/stades.js — à compléter
const express = require('______');      // (1)
const router  = express.______();       // (2)
const fs      = require('fs');
const path    = require('path');

const FICHIER = path.join(__dirname, '..', 'data', 'stades.json');

router.____('/stades', function(req, res) {  // (3)
  fs.readFile(FICHIER, '____', function(erreur, contenu) {  // (4)
    if (______) {                             // (5)
      return res.status(____).json({ erreur: 'Erreur serveur' });  // (6)
    }
    const donnees = JSON.______(contenu);    // (7)
    res.status(200).______({ stades: donnees.stades });  // (8)
  });
});

router.get('/stades/:nom', function(req, res) {
  const nomDemande = req.______.nom;          // (9)
  fs.readFile(FICHIER, 'utf8', function(erreur, contenu) {
    if (erreur) { return res.status(500).json({ erreur: 'Erreur serveur' }); }
    const stades = JSON.parse(contenu).stades;
    const stade  = stades.find(s => s.nom === ______);  // (10)
    if (!stade) {
      return res.status(______).json({ erreur: 'Stade introuvable' });  // (11)
    }
    res.______(200).json({ stade });          // (12)
  });
});

module.exports = ______;                      // (13)
✓ Corrigé
  • (1) 'express' — minuscules obligatoires
  • (2) Router() — crée un mini-routeur
  • (3) get — route GET
  • (4) 'utf8' — encodage pour lire comme texte
  • (5) erreur — tester l'erreur en premier (error-first)
  • (6) 500 — erreur interne serveur
  • (7) parse — JSON.parse() convertit la chaîne en objet
  • (8) json — res.json() envoie la réponse JSON
  • (9) params — req.params.nom pour les paramètres d'URL
  • (10) nomDemande — la variable contenant req.params.nom
  • (11) 404 — ressource introuvable
  • (12) status — res.status(200).json()
  • (13) router — exporter le router
Partie 2 — APIs REST Middleware · POST · PUT
⏱ 16 min

Middleware, POST et PUT

Le middleware Express et son chaînage, express.json(), implémentation des routes POST (créer + valider + écrire dans le fichier JSON) et PUT (modifier partiellement avec Object.assign).

✓ Ce que vous allez apprendre
  • Définition et rôle d'un middleware Express — chaînage et next()
  • Pourquoi req.body est undefined sans express.json()
  • Implémenter POST /equipes — validation + fs.writeFile
  • Implémenter PUT /equipes/:id — fusion partielle avec Object.assign
  • Tester POST et PUT dans Postman avec des corps JSON

1
Le middleware Express — rôle et chaînage
next() · express.json() · Ordre d'exécution
ℹ Définition — Middleware

Un middleware est une fonction qui s'intercale dans le traitement d'une requête HTTP, entre sa réception et l'envoi de la réponse. Il reçoit trois arguments : req (requête), res (réponse), next (passer au middleware suivant). Si next() n'est pas appelé, la chaîne s'interrompt — aucune réponse n'est envoyée.

javascriptmiddlewares-demo.js
// Schéma du chaînage :
// Requête → [express.json()] → [logger] → [route handler] → Réponse

// Middleware 1 : logger — s'applique à TOUTES les routes
app.use(function(req, res, next) {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
  next(); // Indispensable — sinon la requête reste bloquée ici
});

// Middleware 2 : parse le body JSON (middleware fourni par Express)
app.use(express.json());
// Sans cette ligne : req.body === undefined dans POST et PUT

// Route handler — exécutée APRÈS les deux middlewares
app.post('/equipes', function(req, res) {
  console.log(req.body); // Maintenant disponible grâce à express.json()
  res.json({ recu: req.body });
});
⚠ L'ordre des middlewares est crucial

app.use(express.json()) doit être déclaré avant les routes qui utilisent req.body. Express exécute les middlewares dans l'ordre où ils sont définis. Un middleware déclaré après la route ne sera jamais exécuté pour cette route.


2
POST /equipes — créer et écrire dans le fichier
Validation · fs.writeFile · 201 Created

La route POST lit le fichier, ajoute la nouvelle équipe, réécrit le fichier entier, puis répond. Cette séquence doit être strictement respectée — écrire avant de lire corromprait les données.

javascriptroutes/equipes.js — POST
// Fonction utilitaire : réécrire le fichier JSON
function ecrireEquipes(equipes, callback) {
  const donnees   = JSON.stringify({ equipes: equipes }, null, 2);
  fs.writeFile(FICHIER, donnees, 'utf8', callback);
}

// ── POST /equipes ─────────────────────────────────────────
router.post('/', function(req, res) {
  const { nom, ville, fondation, stade, titres_nationaux } = req.body;

  // Validation — les champs obligatoires doivent être présents et valides
  if (!nom || typeof nom !== 'string' || nom.trim() === '') {
    return res.status(400).json({ erreur: 'Le champ nom est obligatoire (string non vide)' });
  }
  if (!ville || typeof ville !== 'string') {
    return res.status(400).json({ erreur: 'Le champ ville est obligatoire' });
  }
  if (fondation !== undefined && (typeof fondation !== 'number' || fondation < 1800)) {
    return res.status(400).json({ erreur: 'fondation doit être un nombre >= 1800' });
  }

  lireEquipes(function(erreur, equipes) {
    if (erreur) return res.status(500).json({ erreur: 'Erreur lecture fichier' });

    // Générer un id unique : max des ids existants + 1
    const nouvelId = equipes.length > 0
      ? Math.max(...equipes.map(e => e.id)) + 1
      : 1;

    const nouvelleEquipe = {
      id:               nouvelId,
      nom:              nom.trim(),
      ville:            ville.trim(),
      fondation:        fondation || null,
      stade:            stade || null,
      titres_nationaux: titres_nationaux || 0
    };

    equipes.push(nouvelleEquipe);

    ecrireEquipes(equipes, function(erreurEcriture) {
      if (erreurEcriture) return res.status(500).json({ erreur: 'Erreur écriture fichier' });
      res.status(201).json({
        message: 'Équipe créée avec succès',
        equipe:  nouvelleEquipe
      });
    });
  });
});

3
PUT /equipes/:id — modifier partiellement
Object.assign · Fusion partielle · findIndex()
ℹ Pourquoi Object.assign ?

Si le client envoie uniquement { titres_nationaux: 23 }, écrire equipes[index] = req.body écraserait tous les autres champs (nom, ville, fondation…) par undefined. Object.assign({}, existant, modifications) fusionne : seuls les champs envoyés sont mis à jour, les autres restent intacts.

javascriptroutes/equipes.js — PUT
// ── PUT /equipes/:id ──────────────────────────────────────
router.put('/:id', function(req, res) {
  const idDemande = parseInt(req.params.id);

  if (isNaN(idDemande)) {
    return res.status(400).json({ erreur: 'L'id doit être un nombre entier' });
  }

  lireEquipes(function(erreur, equipes) {
    if (erreur) return res.status(500).json({ erreur: 'Erreur lecture fichier' });

    // findIndex() retourne l'indice dans le tableau (-1 si non trouvé)
    const index = equipes.findIndex(e => e.id === idDemande);

    if (index === -1) {
      return res.status(404).json({
        erreur:     'Équipe introuvable',
        id_demande: idDemande
      });
    }

    // Fusion partielle : les champs de req.body écrasent uniquement les champs correspondants
    equipes[index] = Object.assign({}, equipes[index], req.body, { id: idDemande });
    // { id: idDemande } garantit que l'id ne peut pas être modifié depuis le body

    ecrireEquipes(equipes, function(erreurEcriture) {
      if (erreurEcriture) return res.status(500).json({ erreur: 'Erreur écriture fichier' });
      res.status(200).json({
        message: 'Équipe modifiée avec succès',
        equipe:  equipes[index]
      });
    });
  });
});
Body envoyéAvec Object.assignSans (écrasement direct)
{ titres_nationaux: 23 }Seul titres_nationaux change — nom, ville, fondation intactsL'équipe n'aurait plus que titres_nationaux — tous les autres champs supprimés
{ stade: "Nouveau stade" }Seul stade changeTous les autres champs supprimés

✓
Vérification des acquis — Middleware et écriture
3 questions
Sans app.use(express.json()), que contient req.body dans une route POST ?
Pourquoi utiliser Math.max(...equipes.map(e => e.id)) + 1 pour générer un nouvel id ?
Quelle est la différence entre .find() et .findIndex() sur un tableau ?
Partie 2 — APIs REST DELETE · Erreurs HTTP
⏱ 14 min

DELETE et gestion des erreurs

Implémentation de DELETE avec filter(), middleware de gestion des erreurs global à 4 paramètres, codes HTTP d'erreur complets, et exercice CRUD sur la ressource Joueurs.

✓ Ce que vous allez apprendre
  • Implémenter DELETE /equipes/:id avec filter()
  • Différence entre splice() et filter() pour supprimer
  • Centraliser la gestion des erreurs avec un middleware à 4 paramètres
  • Codes HTTP d'erreur et leurs contextes exacts
  • Exercice CRUD complet sur la ressource Joueurs

1
DELETE /equipes/:id
filter() · splice() · 200 vs 204
javascriptroutes/equipes.js — DELETE
// ── DELETE /equipes/:id ───────────────────────────────────
router.delete('/:id', function(req, res) {
  const idDemande = parseInt(req.params.id);

  if (isNaN(idDemande)) {
    return res.status(400).json({ erreur: 'L'id doit être un nombre entier' });
  }

  lireEquipes(function(erreur, equipes) {
    if (erreur) return res.status(500).json({ erreur: 'Erreur lecture fichier' });

    const equipeASupprimer = equipes.find(e => e.id === idDemande);

    if (!equipeASupprimer) {
      return res.status(404).json({
        erreur:     'Équipe introuvable',
        id_demande: idDemande
      });
    }

    // filter() crée un NOUVEAU tableau sans l'équipe à supprimer
    // Ne modifie pas le tableau original — plus sûr que splice()
    const equipesRestantes = equipes.filter(e => e.id !== idDemande);

    ecrireEquipes(equipesRestantes, function(erreurEcriture) {
      if (erreurEcriture) return res.status(500).json({ erreur: 'Erreur écriture fichier' });
      res.status(200).json({
        message: 'Équipe supprimée avec succès',
        equipe:  equipeASupprimer // Confirmer ce qui a été supprimé
      });
    });
  });
});
MéthodeComportementRecommandation
filter()Crée un nouveau tableau sans l'élément — tableau original inchangé Recommandé — immutable, lisible
splice(index, 1)Supprime 1 élément du tableau original en place⚠ Fonctionne mais mutate le tableau — peut causer des bugs

2
Middleware de gestion des erreurs global
4 paramètres · next(err) · Format uniforme
ℹ Middleware d'erreur — signature spéciale

Express reconnaît un middleware d'erreur à sa signature à 4 paramètres : (err, req, res, next). Il est déclaré après toutes les routes dans index.js. Il est déclenché quand une route appelle next(new Error(...)).

javascriptindex.js — middleware d'erreur global
// Routes montées ici...
app.use('/equipes', equipesRouter);

// Route 404 — aucune route précédente ne correspond
app.use(function(req, res) {
  res.status(404).json({
    erreur: 'Route introuvable',
    url:    req.url,
    methode: req.method
  });
});

// Middleware d'erreur — TOUJOURS déclaré en dernier, TOUJOURS 4 paramètres
app.use(function(err, req, res, next) {
  console.error('[ERREUR]', err.message);
  res.status(err.status || 500).json({
    erreur:  err.message || 'Erreur interne du serveur',
    details: process.env.NODE_ENV === 'development' ? err.stack : undefined
  });
});

// Dans une route, déclencher le middleware d'erreur :
router.get('/:id', function(req, res, next) {
  lireEquipes(function(err, equipes) {
    if (err) return next(err); // Passe l'erreur au middleware global
    // ...
  });
});
CodeSignificationExemple de déclenchement
400Bad Request — données invalidesid non numérique, champ obligatoire absent, valeur hors plage
401Unauthorized — non authentifiéToken JWT absent dans les headers
403Forbidden — authentifié mais sans droitsUtilisateur lambda tente de supprimer un enregistrement admin
404Not Found — ressource introuvableGET /equipes/99 — aucune équipe avec cet id
409Conflict — conflit de donnéesPOST /equipes avec un nom déjà existant
500Internal Server Error — bug serveurErreur de lecture/écriture fichier, exception non gérée

3
CRUD complet — récapitulatif de l'API équipes
Toutes les routes · Codes HTTP · Cas d'erreur
RouteActionSuccèsErreurs possibles
GET /equipesLire toutes les équipes200 + tableau500 (lecture fichier)
GET /equipes/:idLire une équipe200 + équipe400 (id non numérique), 404 (id inexistant), 500
POST /equipesCréer une équipe201 + nouvelle équipe400 (validation échoue), 500 (écriture)
PUT /equipes/:idModifier une équipe200 + équipe modifiée400 (id), 404 (inexistant), 500
DELETE /equipes/:idSupprimer une équipe200 + équipe supprimée400 (id), 404 (inexistant), 500

Exercice — CRUD Joueurs

Créer une ressource joueurs complète dans votre projet, en suivant exactement le même pattern que equipes :

  • Créer data/joueurs.json avec au moins 5 joueurs marocains (id, nom, poste, equipe_id, nationalite)
  • Créer routes/joueurs.js avec les 5 routes CRUD
  • Monter le router dans index.js : app.use('/joueurs', joujeursRouter)
  • Ajouter une route bonus : GET /equipes/:id/joueurs — retourner les joueurs d'une équipe spécifique
  • Tester les 5 routes + la route bonus dans Postman
Partie 2 — APIs REST MongoDB · MongoClient
⏱ 16 min

Introduction à MongoDB

Pourquoi MongoDB remplace les fichiers JSON, vocabulaire SQL vs MongoDB, installation, connexion Node.js avec MongoClient, premier script find() et intégration dans le projet Express.

✓ Ce que vous allez apprendre
  • MongoDB vs SQL — vocabulaire, structure et différences fondamentales
  • Installer MongoDB et MongoDB Compass
  • Créer une base bdmonapi et une collection equipe
  • Se connecter à MongoDB depuis Node.js avec le package mongodb
  • Lire des documents MongoDB depuis une route Express

1
Pourquoi MongoDB ? Limites du fichier JSON
Concurrence · Performance · Documents natifs
⚠ Deux problèmes critiques des fichiers JSON en production
  • Concurrence : deux requêtes simultanées peuvent lire le même fichier, générer le même id et écrire en même temps — les données se corrompent.
  • Performance : chaque lecture requiert de charger tout le fichier en mémoire. Avec 10 000 équipes, chaque GET lit 10 000 documents pour en retourner un seul.
Concept SQLÉquivalent MongoDBDifférence clé
Base de donnéesBase de donnéesMême concept — un conteneur de données nommé
TableCollectionUne collection stocke des documents JSON — pas des lignes uniformes
Ligne (row)DocumentUn document est un objet JSON — chaque document peut avoir des champs différents
Colonne (column)Champ (field)Les champs ne sont pas fixes — un document peut avoir des champs absents
Clé primaire (id)_id (ObjectId)MongoDB génère automatiquement un _id unique (24 caractères hexadécimaux)
JOIN (relation)Document imbriqué ou $lookupMongoDB favorise les documents imbriqués plutôt que les jointures
ℹ Pourquoi MongoDB pour les APIs REST Node.js ?
  • Documents = JSON natif : les données que l'API manipule sont déjà en JSON. MongoDB les stocke tels quels — pas de conversion.
  • Schéma flexible : ajouter un nouveau champ à un document ne nécessite pas d'ALTER TABLE.
  • Scalabilité horizontale : conçu pour être distribué sur plusieurs serveurs — adapté au Cloud Native.
  • Popularité Node.js : MongoDB est le choix de base de la stack MERN (MongoDB, Express, React, Node.js).

2
Installation et configuration
MongoDB Community · Compass · npm mongodb
bashterminal
# 1. Vérifier que MongoDB est démarré
mongod --version  # Ex: db version v7.0.4

# 2. Installer le driver officiel Node.js
npm install mongodb

# Structure du projet après ajout MongoDB :
# api-equipes/
# ├── config/
# │   └── mongodb.js   ← nouveau : gestion connexion
# ├── routes/
# │   └── equipes.js   ← sera mis à jour pour MongoDB
# └── index.js
javascriptconfig/mongodb.js
const { MongoClient } = require('mongodb');

// Chaîne de connexion — en production, toujours dans une variable d'environnement
const URI  = process.env.MONGO_URI || 'mongodb://localhost:27017';
const DBNAME = 'bdmonapi';

let base; // Variable qui stocke la connexion — réutilisée par toutes les routes

async function connecter() {
  try {
    const client = new MongoClient(URI);
    await client.connect();
    base = client.db(DBNAME);
    console.log(`MongoDB connecté : ${DBNAME}`);
  } catch (err) {
    console.error('Erreur connexion MongoDB :', err.message);
    process.exit(1); // Arrêter le serveur si MongoDB est inaccessible
  }
}

function getBase() {
  if (!base) throw new Error('MongoDB non connecté — appeler connecter() d'abord');
  return base;
}

module.exports = { connecter, getBase };
javascriptindex.js — démarrage avec MongoDB
const express        = require('express');
const { connecter }  = require('./config/mongodb');
const equipesRouter  = require('./routes/equipes');

const app  = express();
const PORT = 3000;

app.use(express.json());
app.use('/equipes', equipesRouter);

// Connecter MongoDB AVANT de démarrer Express
// Garantit que la base est prête avant d'accepter des requêtes
connecter().then(function() {
  app.listen(PORT, function() {
    console.log(`Serveur : http://localhost:${PORT}`);
  });
});

3
Premier script Node.js ↔ MongoDB
find() · toArray() · Cursor
javascripttest-mongo.js
const { MongoClient } = require('mongodb');

async function main() {
  const client = new MongoClient('mongodb://localhost:27017');

  try {
    await client.connect();
    console.log('Connexion établie');

    const base       = client.db('bdmonapi');
    const collection = base.collection('equipe');

    // find({}) = tous les documents — {} = filtre vide = aucun filtre
    // find() retourne un CURSOR, pas les données directement
    // toArray() convertit le cursor en tableau JavaScript
    const equipes = await collection.find({}).toArray();
    console.log('Équipes trouvées :', equipes.length);
    equipes.forEach(e => console.log('-', e.nom, '|', e.ville));

  } finally {
    await client.close(); // Toujours fermer la connexion
    console.log('Connexion fermée');
  }
}

main().catch(console.error);
ℹ Cursor vs toArray()

find() retourne un Cursor — un pointeur sur les résultats, pas les données elles-mêmes. Cela permet à MongoDB d'envoyer les données par lots (streaming) sans tout charger en mémoire. toArray() parcourt le cursor et convertit tous les documents en tableau JavaScript. Pour de très grandes collections, préférer cursor.forEach() qui traite les documents un par un.


✓
Comparaison SQL vs MongoDB — associez les concepts
Tableau de correspondance
Opération SQLÉquivalent MongoDB (Node.js)
SELECT * FROM equipecollection.find({}).toArray()
SELECT * FROM equipe WHERE id = 3collection.findOne({ _id: objectId })
INSERT INTO equipe VALUES (...)collection.insertOne({ nom: '...', ville: '...' })
UPDATE equipe SET titres = 23 WHERE id = 1collection.updateOne({ _id: id }, { $set: { titres_nationaux: 23 } })
DELETE FROM equipe WHERE id = 3collection.deleteOne({ _id: objectId })
SELECT * FROM equipe WHERE ville = 'Rabat'collection.find({ ville: 'Rabat' }).toArray()
Partie 2 — APIs REST MongoDB · find · insertOne
⏱ 15 min

Routes GET et POST sur MongoDB

ObjectId et sa validation, routes GET avec find()/sort(), GET par id avec findOne(), POST avec insertOne() — migration complète des routes JSON vers MongoDB.

✓ Ce que vous allez apprendre
  • ObjectId — comprendre et convertir les identifiants MongoDB
  • GET /equipes avec find(), projection et tri
  • GET /equipes/:id avec findOne() par ObjectId
  • POST /equipes avec insertOne() et validation
  • Tester dans Postman et vérifier dans MongoDB Compass

1
ObjectId — l'identifiant MongoDB
Format · Validation · Conversion
ℹ Qu'est-ce qu'un ObjectId ?

En MongoDB, chaque document a un champ _id de type ObjectId — une chaîne hexadécimale de 24 caractères générée automatiquement. Ex : 65f4a1b2c3d4e5f6a7b8c9d0. Ce n'est pas un entier auto-incrémenté comme en SQL — c'est un identifiant unique garanti même sur plusieurs serveurs distribués.

javascriptroutes/equipes.js — ObjectId
const { ObjectId } = require('mongodb');
const { getBase }  = require('../config/mongodb');

// Fonction utilitaire : valider et convertir un id depuis req.params
function parseObjectId(idString) {
  if (!ObjectId.isValid(idString)) {
    return null; // Format invalide — déclencher un 400
  }
  return new ObjectId(idString);
}
// Usage : const objectId = parseObjectId(req.params.id);
//         if (!objectId) return res.status(400).json({ erreur: 'Id invalide' });
⚠ Toujours valider avant de convertir

new ObjectId('abc') lève une BSONError si la chaîne n'est pas un ObjectId valide. Appeler ObjectId.isValid(id) avant new ObjectId(id) évite un crash serveur (500) pour une erreur client (400).


2
GET /equipes — find(), projection et tri
find() · sort() · project() · toArray()
javascriptroutes/equipes.js — GET liste
// ── GET /equipes ─────────────────────────────────────────
router.get('/', async function(req, res) {
  try {
    const collection = getBase().collection('equipe');

    // Options de requête depuis les query params
    const ville = req.query.ville;   // Filtre optionnel : /equipes?ville=Rabat
    const tri   = req.query.tri || 'nom'; // Tri par défaut sur le nom

    const filtre = ville ? { ville: ville } : {};

    const equipes = await collection
      .find(filtre)
      .sort({ [tri]: 1 })    // 1 = croissant, -1 = décroissant
      .toArray();

    res.status(200).json({
      total:   equipes.length,
      equipes: equipes
    });
  } catch (err) {
    res.status(500).json({ erreur: 'Erreur serveur MongoDB' });
  }
});

3
GET /equipes/:id — findOne() par ObjectId
findOne() · null check · 404
javascriptroutes/equipes.js — GET par id
// ── GET /equipes/:id ─────────────────────────────────────
router.get('/:id', async function(req, res) {
  const objectId = parseObjectId(req.params.id);
  if (!objectId) {
    return res.status(400).json({ erreur: 'Format d'id invalide (ObjectId attendu)' });
  }

  try {
    const collection = getBase().collection('equipe');
    const equipe     = await collection.findOne({ _id: objectId });

    // findOne() retourne null si aucun document ne correspond — pas d'exception
    if (!equipe) {
      return res.status(404).json({
        erreur: 'Équipe introuvable',
        id:     req.params.id
      });
    }

    res.status(200).json({ equipe });
  } catch (err) {
    res.status(500).json({ erreur: 'Erreur serveur MongoDB' });
  }
});

4
POST /equipes — insertOne()
insertOne() · insertedId · acknowledged
javascriptroutes/equipes.js — POST MongoDB
// ── POST /equipes ────────────────────────────────────────
router.post('/', async function(req, res) {
  const { nom, ville, fondation, stade, titres_nationaux } = req.body;

  if (!nom || !ville) {
    return res.status(400).json({ erreur: 'nom et ville sont obligatoires' });
  }

  const nouvelleEquipe = {
    nom:              nom.trim(),
    ville:            ville.trim(),
    fondation:        fondation || null,
    stade:            stade || null,
    titres_nationaux: titres_nationaux || 0,
    createdAt:        new Date()  // MongoDB peut stocker des dates natives
  };

  try {
    const collection = getBase().collection('equipe');
    const resultat   = await collection.insertOne(nouvelleEquipe);

    // insertOne() retourne { acknowledged: true, insertedId: ObjectId(...) }
    res.status(201).json({
      message:  'Équipe créée avec succès',
      insertedId: resultat.insertedId,
      equipe:   { _id: resultat.insertedId, ...nouvelleEquipe }
    });
  } catch (err) {
    res.status(500).json({ erreur: 'Erreur insertion MongoDB' });
  }
});
Propriété de retourTypeValeur
acknowledgedbooleantrue si MongoDB a confirmé l'insertion
insertedIdObjectIdL'_id généré automatiquement pour le nouveau document

✓
Vérification — API MongoDB vs fichier JSON
3 questions
Que retourne collection.findOne({ _id: objectId }) si aucun document ne correspond ?
Pourquoi appelle-t-on ObjectId.isValid(id) avant new ObjectId(id) ?
Partie 2 — APIs REST updateOne · deleteOne · $set
⏱ 14 min

PUT et DELETE sur MongoDB

updateOne() avec $set, deleteOne() avec deletedCount, operateurs MongoDB, bilan JSON vs MongoDB, TP consolidation CRUD complet sur la ressource Stades.

✓ Ce que vous allez apprendre
  • PUT /equipes/:id avec updateOne() et l'opérateur $set
  • DELETE /equipes/:id avec deleteOne() et deletedCount
  • Les opérateurs MongoDB : $set, $inc, $push, $pull
  • Bilan comparatif API JSON vs API MongoDB
  • TP consolidation CRUD complet sur la ressource Stades

1
PUT /equipes/:id — updateOne() et $set
updateOne() · $set · matchedCount · modifiedCount
⚠ L'opérateur $set est obligatoire

updateOne({ _id: id }, { titres_nationaux: 23 }) lève une erreur MongoDB : "update document must use update operators". MongoDB interdit de passer un document brut — il faut obligatoirement { $set: { titres_nationaux: 23 } }. Avec $set, seul le champ spécifié est modifié — tous les autres restent intacts.

javascriptroutes/equipes.js — PUT MongoDB
// ── PUT /equipes/:id ─────────────────────────────────────
router.put('/:id', async function(req, res) {
  const objectId = parseObjectId(req.params.id);
  if (!objectId) {
    return res.status(400).json({ erreur: 'Format d'id invalide' });
  }

  if (Object.keys(req.body).length === 0) {
    return res.status(400).json({ erreur: 'Body vide — aucune modification fournie' });
  }

  // Empêcher la modification du champ _id
  const { _id, ...modifications } = req.body;

  try {
    const collection = getBase().collection('equipe');

    const resultat = await collection.updateOne(
      { _id: objectId },          // Filtre : quel document modifier
      { $set: modifications }     // Opérateur $set : modifier uniquement les champs fournis
    );

    // matchedCount = 0 → aucun document avec cet _id
    if (resultat.matchedCount === 0) {
      return res.status(404).json({ erreur: 'Équipe introuvable' });
    }

    // Récupérer le document mis à jour pour le renvoyer
    const equipeMAJ = await collection.findOne({ _id: objectId });
    res.status(200).json({
      message: 'Équipe modifiée avec succès',
      equipe:  equipeMAJ
    });
  } catch (err) {
    res.status(500).json({ erreur: 'Erreur MongoDB' });
  }
});
OpérateurEffetExemple
$setModifier des champs spécifiques{ $set: { titres_nationaux: 23 } }
$unsetSupprimer un champ du document{ $unset: { stade: '' } }
$incIncrémenter un champ numérique{ $inc: { titres_nationaux: 1 } }
$pushAjouter un élément à un tableau{ $push: { joueurs: 'Ilias Akhomach' } }
$pullRetirer un élément d'un tableau{ $pull: { joueurs: 'Ilias Akhomach' } }

2
DELETE /equipes/:id — deleteOne()
deleteOne() · deletedCount · 404
javascriptroutes/equipes.js — DELETE MongoDB
// ── DELETE /equipes/:id ──────────────────────────────────
router.delete('/:id', async function(req, res) {
  const objectId = parseObjectId(req.params.id);
  if (!objectId) {
    return res.status(400).json({ erreur: 'Format d'id invalide' });
  }

  try {
    const collection = getBase().collection('equipe');

    // Récupérer l'équipe avant suppression pour la confirmer dans la réponse
    const equipe = await collection.findOne({ _id: objectId });
    if (!equipe) {
      return res.status(404).json({ erreur: 'Équipe introuvable' });
    }

    const resultat = await collection.deleteOne({ _id: objectId });

    // deletedCount = 0 si aucun document supprimé (race condition possible)
    if (resultat.deletedCount === 0) {
      return res.status(500).json({ erreur: 'Suppression échouée' });
    }

    res.status(200).json({
      message: 'Équipe supprimée avec succès',
      equipe:  equipe // Confirmer ce qui a été supprimé
    });
  } catch (err) {
    res.status(500).json({ erreur: 'Erreur MongoDB' });
  }
});

3
Bilan — API JSON vs API MongoDB
Comparaison des deux approches
CritèreAPI JSON (fichiers)API MongoDB
InstallationAucune — fichiers déjà làMongoDB à installer + configurer
ConcurrenceDangereuse — corruption possibleGérée nativement par MongoDB
PerformanceLit tout le fichier à chaque requêteIndex, requêtes ciblées, scalable
IdentifiantsEntiers auto-incrémentés manuelsObjectId générés automatiquement
Requêtes avancéesFiltre manuel en JS (.filter())Opérateurs MongoDB ($gte, $in, $regex...)
ProductionNon — données perdues si crashOui — persistance garantie
Cas d'usagePrototypage rapide, séances 5-7Production, séances 8-14

✓
Questions de consolidation
Vérification des acquis
Que retourne updateOne() si le filtre ne correspond à aucun document ?
Quelle est la différence entre $set et un remplacement direct dans updateOne() ?
Partie 2 — APIs REST JWT · bcrypt · dotenv
⏱ 16 min

Authentification JWT — register et login

Sessions vs tokens, structure d'un JWT (header/payload/signature), installation jsonwebtoken et bcryptjs, routes POST /auth/register (hashage bcrypt) et POST /auth/login (generation du token).

✓ Ce que vous allez apprendre
  • Pourquoi JWT plutôt que les sessions dans une API REST stateless
  • Structure d'un JWT : header, payload, signature
  • Installation : jsonwebtoken, bcryptjs, dotenv
  • POST /auth/register : validation, hashage bcrypt, stockage MongoDB
  • POST /auth/login : vérification mot de passe et génération du token

1
Sessions vs tokens — pourquoi JWT ?
Stateless · Scalabilite · Load balancer

HTTP est un protocole sans etat : chaque requete est independante, le serveur ne se souvient pas des precedentes. Deux approches existent pour maintenir l'identite d'un utilisateur entre les requetes :

CritereSessions (cookie + serveur)JWT (token)
StockageCote serveur en memoire ou BDDCote client uniquement
ScalabiliteProblematique avec load balancerParfaite — chaque serveur verifie le token seul
CompatibiliteNavigateur uniquement (cookie)Mobile, SPA, APIs tierces
RevocationImmediate (supprimer la session)Complexe — token valide jusqu'a expiration
Architecture RESTViole le principe statelessRespecte le principe stateless
ℹ Pourquoi les sessions posent probleme avec un load balancer

Si 3 serveurs tournent en parallele et que la session est stockee en memoire du serveur A, la requete suivante traitee par le serveur B ne trouve pas la session. Avec JWT, le token porte l'information — n'importe quel serveur peut le verifier avec la cle secrete partagee.


2
Structure d'un JWT
Header · Payload · Signature · Base64URL
ℹ Un JWT = 3 parties separees par des points

eyJhbGci.eyJ1c2VySWQi.HMAC_SHA256

  • Header : algorithme (HS256) et type (JWT) — encode en Base64URL
  • Payload : donnees utilisateur (userId, role, exp) — encode en Base64URL. Lisible par tous, jamais de mot de passe ici.
  • Signature : HMAC-SHA256(header + payload, JWT_SECRET) — seul element qui necessite le secret. Garantit que le token n'a pas ete altere.
⚠ Le payload est lisible — pas chiffre

Base64URL est un encodage, pas un chiffrement. N'importe qui peut decoder le payload. Ne jamais y mettre de mot de passe, de donnee bancaire ou d'information sensible.


3
Installation et configuration
jsonwebtoken · bcryptjs · dotenv · .env
bashterminal
npm install jsonwebtoken bcryptjs dotenv
text.env — ne jamais versionner sur Git
MONGO_URI=mongodb://localhost:27017
JWT_SECRET=une_chaine_aleatoire_longue_et_complexe_min_32_chars
JWT_EXPIRES_IN=7d
PORT=3000
javascriptindex.js — charger dotenv en premier
require('dotenv').config(); // TOUJOURS en premiere ligne
const express = require('express');
// ...

4
POST /auth/register et POST /auth/login
bcrypt.hash · bcrypt.compare · jwt.sign
javascriptroutes/auth.js
const express  = require('express');
const router   = express.Router();
const bcrypt   = require('bcryptjs');
const jwt      = require('jsonwebtoken');
const { getBase } = require('../config/mongodb');

// ── POST /auth/register ───────────────────────────────────
router.post('/register', async function(req, res) {
  const { email, motDePasse, role } = req.body;

  if (!email || !motDePasse) {
    return res.status(400).json({ erreur: 'email et motDePasse obligatoires' });
  }

  const collection = getBase().collection('utilisateur');

  // Verifier que l'email n'existe pas deja
  const existant = await collection.findOne({ email });
  if (existant) {
    return res.status(409).json({ erreur: 'Email deja utilise' });
  }

  // Hasher le mot de passe — saltRounds=12 recommande en production
  const hash = await bcrypt.hash(motDePasse, 12);

  const utilisateur = {
    email,
    motDePasse: hash,           // Stocker le hash, jamais le mot de passe brut
    role:       role || 'user', // Defaut : user, pas admin
    createdAt:  new Date()
  };

  const resultat = await collection.insertOne(utilisateur);
  res.status(201).json({
    message: 'Compte cree avec succes',
    userId:  resultat.insertedId
  });
});

// ── POST /auth/login ──────────────────────────────────────
router.post('/login', async function(req, res) {
  const { email, motDePasse } = req.body;

  if (!email || !motDePasse) {
    return res.status(400).json({ erreur: 'email et motDePasse obligatoires' });
  }

  const collection   = getBase().collection('utilisateur');
  const utilisateur  = await collection.findOne({ email });

  // Meme message si email inexistant ou mot de passe incorrect
  // Evite de reveler quels emails existent en base
  if (!utilisateur) {
    return res.status(401).json({ erreur: 'Identifiants incorrects' });
  }

  const motDePasseValide = await bcrypt.compare(motDePasse, utilisateur.motDePasse);
  if (!motDePasseValide) {
    return res.status(401).json({ erreur: 'Identifiants incorrects' });
  }

  // Generer le JWT
  const token = jwt.sign(
    { userId: utilisateur._id, email: utilisateur.email, role: utilisateur.role },
    process.env.JWT_SECRET,
    { expiresIn: process.env.JWT_EXPIRES_IN || '7d' }
  );

  res.status(200).json({
    message: 'Connexion reussie',
    token:   token  // Le client stocke ce token et l'envoie dans Authorization: Bearer 
  });
});

module.exports = router;
✓ Bonne pratique securite — meme message d'erreur

Renvoyer le meme message "Identifiants incorrects" si l'email n'existe pas ET si le mot de passe est faux. Sinon un attaquant peut enumerer les emails valides : message different = email connu.


✓
Verification des acquis
3 questions
Quelle partie d'un JWT necessite JWT_SECRET pour etre lue ?
Pourquoi stocker le hash bcrypt et non le mot de passe brut en base ?
Partie 2 — APIs REST isAuthenticated · isAdmin · JWT
⏱ 15 min

Middleware Auth et Autorisation

Middleware isAuthenticated avec jwt.verify(), gestion des erreurs TokenExpiredError/JsonWebTokenError, middleware isAdmin (403), chainer les middlewares sur les routes, flux complet Postman.

✓ Ce que vous allez apprendre
  • Middleware isAuthenticated : extraire et verifier le token avec jwt.verify()
  • Gestion des erreurs JWT : TokenExpiredError, JsonWebTokenError
  • Placer le payload decode dans req.utilisateur
  • Middleware isAdmin : autorisation basee sur le role
  • Flux complet Postman : register → login → route protegee

1
Middleware isAuthenticated
Authorization header · Bearer · jwt.verify · req.utilisateur
javascriptmiddlewares/isAuthenticated.js
const jwt = require('jsonwebtoken');

function isAuthenticated(req, res, next) {
  const authHeader = req.headers.authorization;

  // Cas 1 : header absent
  if (!authHeader) {
    return res.status(401).json({ erreur: 'Token manquant — Authorization: Bearer <token>' });
  }

  // Cas 2 : format invalide (doit etre "Bearer <token>")
  const parties = authHeader.split(' ');
  if (parties.length !== 2 || parties[0] !== 'Bearer') {
    return res.status(401).json({ erreur: 'Format invalide — utiliser Authorization: Bearer <token>' });
  }

  const token = parties[1];

  try {
    // jwt.verify() decode ET verifie la signature + expiration
    const payload = jwt.verify(token, process.env.JWT_SECRET);
    req.utilisateur = payload; // Disponible dans toutes les routes suivantes
    next(); // Passer a la route handler
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ erreur: 'Token expire — reconnectez-vous' });
    }
    // JsonWebTokenError, NotBeforeError ou autre
    return res.status(401).json({ erreur: 'Token invalide' });
  }
}

module.exports = isAuthenticated;
CasErreur jwt.verify()Code HTTP
Token absent—401
Format non Bearer—401
Signature incorrecteJsonWebTokenError401
Token expireTokenExpiredError401
Token valide, role insuffisant—403

2
Middleware isAdmin et protection des routes
req.utilisateur.role · 403 Forbidden · Chainer les middlewares
javascriptmiddlewares/isAdmin.js
// isAdmin doit toujours etre utilise APRES isAuthenticated
function isAdmin(req, res, next) {
  // req.utilisateur est place par isAuthenticated
  if (!req.utilisateur || req.utilisateur.role !== 'admin') {
    return res.status(403).json({
      erreur: 'Acces reserve aux administrateurs'
    });
  }
  next();
}

module.exports = isAdmin;
javascriptroutes/equipes.js — protection ciblée
const isAuthenticated = require('../middlewares/isAuthenticated');
const isAdmin         = require('../middlewares/isAdmin');

// GET public — pas de protection
router.get('/', async function(req, res) { /* ... */ });

// PUT protege — utilisateur authentifie requis
router.put('/:id', isAuthenticated, async function(req, res) { /* ... */ });

// DELETE protege — admin uniquement
router.delete('/:id', isAuthenticated, isAdmin, async function(req, res) { /* ... */ });
// L'ordre compte : isAuthenticated s'execute avant isAdmin
// Si le token est invalide, isAuthenticated bloque et isAdmin ne s'execute jamais

3
Flux complet Postman
register → login → route protegee
EtapeRequete PostmanBodyResultat attendu
1POST /auth/register{"email":"test@ma.ma","motDePasse":"Secret123"}201 + userId
2POST /auth/login{"email":"test@ma.ma","motDePasse":"Secret123"}200 + token
3DELETE /equipes/:id—401 (pas de token)
4DELETE /equipes/:id + Authorization: Bearer <token>—403 si role=user, 200 si role=admin

✓
Verification des acquis
3 questions
Un utilisateur authentifie (role='user') tente DELETE /equipes/:id protegee par isAdmin. Quel code HTTP recoit-il ?
jwt.verify() lance TokenExpiredError. Que s'est-il passe ?
Partie 2 — APIs REST helmet · CORS · rate-limit · validator
⏱ 14 min

Securite REST avancee

helmet (headers securises), CORS strict, rate limiting anti-brute-force, validation des entrees avec express-validator, tableau des attaques et protections, architecture finale du projet.

✓ Ce que vous allez apprendre
  • Middleware global d'erreurs avec classes d'erreurs personnalisees
  • helmet : securiser les headers HTTP automatiquement
  • CORS : controler quels origines peuvent appeler l'API
  • Rate limiting : bloquer le brute force sur /auth/login
  • Validation des entrees avec express-validator

1
helmet — securiser les headers HTTP
X-Powered-By · CSP · HSTS · XSS-Protection

Par defaut, Express expose des headers qui revele l'infrastructure : X-Powered-By: Express indique a un attaquant le framework utilise. helmet supprime ces headers et en ajoute d'autres qui renforcent la securite.

bashterminal
npm install helmet cors express-rate-limit express-validator
javascriptindex.js — securite globale
const helmet      = require('helmet');
const cors        = require('cors');
const rateLimit   = require('express-rate-limit');

// helmet : applique ~15 headers de securite en une ligne
app.use(helmet());
// Supprime X-Powered-By, ajoute X-Content-Type-Options, X-Frame-Options, etc.

// CORS : autoriser uniquement les origines connues
app.use(cors({
  origin:  ['http://localhost:4200', 'https://mon-frontend.ma'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

// Rate limiting general — 100 req par IP par 15 min
const limiterGeneral = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15 minutes
  max:      100,
  message:  { erreur: 'Trop de requetes — reessayez dans 15 minutes' }
});
app.use(limiterGeneral);

// Rate limiting strict sur login — 10 tentatives par IP par 15 min
const limiterLogin = rateLimit({
  windowMs: 15 * 60 * 1000,
  max:      10,
  message:  { erreur: 'Trop de tentatives de connexion — reessayez dans 15 minutes' }
});
app.use('/auth/login', limiterLogin);

2
Validation des entrees avec express-validator
body() · validationResult() · Injection prevention
javascriptroutes/auth.js — avec validation
const { body, validationResult } = require('express-validator');

// Regles de validation pour /register
const reglesRegister = [
  body('email')
    .isEmail()
    .normalizeEmail()
    .withMessage('Email invalide'),
  body('motDePasse')
    .isLength({ min: 8 })
    .withMessage('Mot de passe : 8 caracteres minimum')
    .matches(/[A-Z]/)
    .withMessage('Mot de passe : au moins une majuscule')
];

router.post('/register', reglesRegister, async function(req, res) {
  // Verifier les erreurs de validation
  const erreurs = validationResult(req);
  if (!erreurs.isEmpty()) {
    return res.status(400).json({
      erreur:  'Donnees invalides',
      details: erreurs.array() // Liste de toutes les erreurs de validation
    });
  }
  // Suite du code register...
});
AttaqueProtection
Brute force loginRate limiting strict sur /auth/login
Enumeration d'emailsMeme message d'erreur pour email inexistant et mot de passe faux
Injection NoSQLexpress-validator sanitise les entrees avant qu'elles atteignent MongoDB
XSS via APIhelmet + Content-Security-Policy header
CSRFCORS strict limite les origines autorisees
Exposition infrahelmet supprime X-Powered-By et les headers revelateurs

✓
Recapitulatif Partie 2 — API REST complete
Architecture finale du projet
textStructure finale du projet api-equipes
api-equipes/
├── config/
│   └── mongodb.js         connecter(), getBase()
├── middlewares/
│   ├── isAuthenticated.js  jwt.verify(), req.utilisateur
│   └── isAdmin.js          role === admin
├── routes/
│   ├── auth.js             POST /auth/register, POST /auth/login
│   ├── equipes.js          CRUD complet MongoDB
│   └── stades.js           CRUD complet MongoDB
├── .env                JWT_SECRET, MONGO_URI, PORT
├── .gitignore          node_modules/, .env
├── package.json
└── index.js            helmet, cors, rateLimit, routes
Pourquoi appliquer un rate limiting plus strict sur /auth/login (10 req/15 min) que sur les autres routes (100 req/15 min) ?
Partie 3 — Microservices API Gateway · Circuit Breaker · Saga
⏱ 15 min

Architecture Microservices — Concepts

Decomposition du monolithe en services autonomes, API Gateway avec http-proxy-middleware, Database per Service, patterns Circuit Breaker et Saga, criteres de decision monolithe vs microservices.

✓ Ce que vous allez apprendre
  • Decomposition de l'API equipes en services autonomes
  • Couplage faible, contrat d'interface, service autonome
  • API Gateway — role et implementation simplifiee
  • Pattern Database per Service — pourquoi chaque service a sa propre base
  • Patterns Saga et Circuit Breaker
  • Quand adopter les microservices — criteres de decision

1
Decomposition en microservices
Single Responsibility · Frontiere de service · API comme contrat

L'API equipes developpee en Partie 2 est un monolithe qui gere authentification, equipes, stades et competitions dans un seul processus. La decomposer en microservices signifie creer un processus Node.js independant par domaine fonctionnel.

ServiceResponsabilite uniquePortBase de donnees
auth-serviceRegister, login, verification JWT3001bdauth — collection utilisateur
equipe-serviceCRUD complet sur les equipes3002bdequipes — collection equipe
stade-serviceCRUD complet sur les stades3003bdstades — collection stade
competition-serviceGestion des competitions et calendriers3004bdcompetitions — collection competition
api-gatewayPoint d'entree unique, routing, auth3000Aucune — proxy uniquement
ℹ Concepts cles
  • Service autonome : deploye, scale et arrete independamment. Si equipe-service plante, auth-service continue.
  • Couplage faible : les services communiquent uniquement via APIs REST ou messages. Aucun partage de code interne.
  • Contrat d'interface : l'API du service est son contrat. Changer l'implementation interne ne casse pas les autres services si le contrat reste identique.

2
API Gateway
Point d'entree unique · Routing · http-proxy-middleware
ℹ Role de l'API Gateway

Le client ne connait qu'une seule URL (http://localhost:3000). L'API Gateway recoit toutes les requetes et les redirige vers le bon service selon le chemin. Elle centralise aussi l'authentification, le logging et le rate limiting.

javascriptapi-gateway/index.js
const express      = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app          = express();

// Routing : redirige vers le bon service selon le chemin
app.use('/auth',        createProxyMiddleware({ target: 'http://localhost:3001', changeOrigin: true }));
app.use('/equipes',     createProxyMiddleware({ target: 'http://localhost:3002', changeOrigin: true }));
app.use('/stades',      createProxyMiddleware({ target: 'http://localhost:3003', changeOrigin: true }));
app.use('/competitions',createProxyMiddleware({ target: 'http://localhost:3004', changeOrigin: true }));

app.listen(3000, () => console.log('API Gateway: http://localhost:3000'));

3
Database per Service et patterns de resilience
Isolation des donnees · Saga · Circuit Breaker

Database per Service

Chaque service possede sa propre base de donnees — jamais partagee avec un autre service. Raisons :

  • Independance de deploiement : modifier le schema de bdequipes n'affecte pas bdauth.
  • Choix technologique libre : equipe-service peut utiliser MongoDB, competition-service peut utiliser PostgreSQL.
  • Isolation des pannes : si bdstades est hors ligne, equipe-service continue de fonctionner.

Circuit Breaker

Quand equipe-service appelle stade-service et que stade-service ne repond pas (timeout), sans Circuit Breaker, equipe-service attend indefiniment et accumule des connexions bloquees. Le Circuit Breaker detecte les echecs consecutifs et "ouvre le circuit" : les appels vers stade-service echouent immediatement pendant un delai, puis un appel de test verifie si le service est de nouveau disponible.

Pattern Saga

Dans un monolithe, une transaction base de donnees garantit la coherence : si une etape echoue, tout est annule (rollback). Avec des services et des bases separees, pas de transaction distribuee. Le pattern Saga decompose une operation en etapes independantes avec des etapes de compensation en cas d'echec.

PatternProbleme resoluPrincipe
Circuit BreakerService en panne bloque les appelantsDetecter les echecs et "ouvrir le circuit" — echouer rapidement plutot qu'attendre
SagaCoherence des donnees sans transaction distribueeEtapes avec compensations — si etape 3 echoue, annuler les etapes 1 et 2
API GatewayClient expose a N services differentsPoint d'entree unique — routing, auth et rate limiting centralises
Database per ServiceServices couples via base partageeChaque service possede sa propre base — couplage uniquement via API

4
Quand adopter les microservices ?
Criteres de decision · Complexite operationnelle · Quand NE PAS le faire
⚠ Les microservices ne sont pas toujours la bonne reponse

Les microservices introduisent une complexite operationnelle significative : plusieurs processus a gerer, communication reseau entre services (latence, pannes), Debug distribue, Docker necessaire pour gerer les environnements. Pour une petite equipe ou un projet en phase initiale, un monolithe bien structure est souvent preferable.

IndicateurMonolitheMicroservices
Taille de l'equipe< 5 developpeurs> 5 developpeurs, equipes separees
Stade du projetPhase initiale, besoins flousProduit etabli, besoins stables
ScalabiliteUniforme — toute l'app ensembleCiblee — scaler uniquement le service charge
Disponibilite requiseTolerante aux interruptions de deploiZero downtime obligatoire
Exemple marocain adapteAPI d'une startup en phase MVPPlateforme Damancom, Inwi (infrastructure etablie)

✓
Verification des acquis
3 questions
Pourquoi chaque microservice doit avoir sa propre base de donnees (Database per Service) ?
Que fait le Circuit Breaker quand un service appele ne repond plus ?
Partie 3 — Microservices API REST vs MS · Communication
⏱ 14 min

API REST vs Microservice — Architecture exemple

6 criteres de distinction API REST / microservice, anti-pattern monolithe distribue, communication synchrone (axios) vs asynchrone (RabbitMQ), architecture 4 services avec health checks.

✓ Ce que vous allez apprendre
  • 6 criteres qui distinguent une API REST d'un microservice
  • Service Discovery — le probleme des adresses dynamiques
  • Communication inter-services synchrone vs asynchrone
  • Architecture de l'application exemple : auth, produit, commande, gateway
  • Structure des 4 services et health checks

1
API REST vs Microservice — 6 criteres
Responsabilite unique · Deployement independant · Base propre
CritereAPI REST classiqueMicroservice
PerimetrePeut couvrir plusieurs domainesUn seul domaine metier (Single Responsibility)
DeploiementDeploye avec le reste de l'applicationDeploye, stoppe et scale independamment
Base de donneesPartage souvent la BDD de l'applicationPossede sa propre BDD — non partagee
Cycle de vieDependant de l'application globaleEquipe et pipeline de deploiement dedies
ScalabiliteScalabilite de toute l'applicationScale uniquement le service charge
DefaillanceUn bug peut impacter tout le systemeLa defaillance reste isolee au service
⚠ Le monolithe distribue — l'anti-pattern a eviter

Couper une application en plusieurs services Node.js qui partagent la meme base de donnees et se couplent via des appels directs internes ne cree pas des microservices — c'est un monolithe distribue. Le resultat : toute la complexite operationnelle des microservices sans aucun de leurs avantages.


2
Communication inter-services
Synchrone HTTP · Asynchrone messages · axios
TypeTechnologieCas d'usageAvantageInconvenient
SynchroneHTTP / axiosVerifier un token, lire un produitSimple, reponse immediateCouplage temporal — si le service appele est hors ligne, l'appelant echoue
AsynchroneRabbitMQ, KafkaEmail de confirmation, decrement stockDecouplage total — resilient aux pannesComplexite, pas de reponse immediate

3
Architecture de l'application exemple
4 services · Ports · Health checks
textStructure du projet
app-microservices/
├── auth-service/      port 3001  → /auth/register, /login, /verify
├── produit-service/   port 3002  → /produits  (CRUD + stock)
├── commande-service/  port 3003  → /commandes (orchestration)
├── gateway/           port 3000  → proxy vers les 3 services
└── shared/
    └── authClient.js     middleware isAuthenticated partage
javascriptExemple — health check sur chaque service
// Route GET /health — presente sur TOUS les services
app.get('/health', async function(req, res) {
  try {
    // Verifier la connexion MongoDB
    await getBase().command({ ping: 1 });
    res.status(200).json({
      status:    'ok',
      service:   'produit-service',
      timestamp: new Date().toISOString()
    });
  } catch (err) {
    res.status(503).json({ status: 'error', message: 'MongoDB indisponible' });
  }
});
Quelle est la difference fondamentale entre un microservice et une API REST classique ?
Partie 3 — Microservices produit-service · pagination · stock
⏱ 15 min

produit-service — CRUD complet

Node.js adapte aux microservices (demarrage, memoire, event loop), produit-service avec filtres dynamiques, pagination skip/limit, champ calcule disponible (stock > 0) pour commande-service.

✓ Ce que vous allez apprendre
  • Pourquoi Node.js est ideal pour les microservices
  • Structure et donnees initiales du produit-service
  • Route GET /produits avec filtres dynamiques et pagination
  • Route GET /produits/:id avec champ calcule disponible
  • Routes POST, PUT, DELETE — validation et gestion des erreurs

1
Pourquoi Node.js pour les microservices
Event loop · Memoire · Demarrage · npm
CaracteristiqueNode.jsJava Spring
Temps de demarrage~100 ms~10-30 secondes
Memoire au repos~30 Mo~300 Mo (JVM)
I/O concurrentEvent loop — milliers de requetesThread pool — limite en nombre
Densite sur serveurDizaines de services par machineQuelques services par machine
ℹ Demarrage rapide — critique pour les microservices

Dans un cluster de microservices, des centaines d'instances peuvent demarrer et s'arreter en quelques secondes (autoscaling). Un demarrage de 100ms vs 30 secondes fait la difference entre une mise a l'echelle fluide et un goulot d'etranglement.


2
produit-service — CRUD complet avec pagination
find() · skip() · limit() · champ calcule disponible
javascriptproduit-service/routes/produits.js
const express     = require('express');
const router      = express.Router();
const { ObjectId } = require('mongodb');
const { getBase } = require('../config/mongodb');

// ── GET /produits — liste avec filtres et pagination ──────
router.get('/', async function(req, res) {
  const { categorie, minPrix, maxPrix, page = 1, limite = 10 } = req.query;

  const filtre = {};
  if (categorie) filtre.categorie = categorie;
  if (minPrix || maxPrix) {
    filtre.prix = {};
    if (minPrix) filtre.prix.$gte = parseFloat(minPrix);
    if (maxPrix) filtre.prix.$lte = parseFloat(maxPrix);
  }

  const skip  = (parseInt(page) - 1) * parseInt(limite);
  const col   = getBase().collection('produit');
  const total = await col.countDocuments(filtre);
  const produits = await col.find(filtre).skip(skip).limit(parseInt(limite)).toArray();

  res.json({
    total,
    page:    parseInt(page),
    pages:   Math.ceil(total / parseInt(limite)),
    produits
  });
});

// ── GET /produits/:id — avec champ calcule disponible ─────
router.get('/:id', async function(req, res) {
  if (!ObjectId.isValid(req.params.id))
    return res.status(400).json({ erreur: 'Id invalide' });

  const produit = await getBase().collection('produit')
    .findOne({ _id: new ObjectId(req.params.id) });

  if (!produit) return res.status(404).json({ erreur: 'Produit introuvable' });

  // Champ calcule : commande-service l'utilisera pour valider la commande
  res.json({ produit: { ...produit, disponible: produit.stock > 0 } });
});

module.exports = router;
Pour afficher la page 3 avec 10 produits par page, quelle valeur de skip utiliser ?
Partie 3 — Microservices auth-service · axios · commande-service
⏱ 16 min

auth-service, commande-service et axios

Endpoint /auth/verify isole JWT_SECRET, appels inter-services axios avec gestion erreur.response vs erreur.code, commande-service qui orchestre auth + produit, denormalisation Database per Service.

✓ Ce que vous allez apprendre
  • auth-service : /register, /login et l'endpoint /verify specifique microservices
  • Appels inter-services avec axios — installation, timeout, gestion d'erreurs
  • commande-service : orchestration auth + produit pour creer une commande
  • Denormalisation — pourquoi copier les donnees du produit dans la commande
  • Tests d'integration des 3 services + Gateway

1
auth-service — endpoint /verify
JWT_SECRET isole · Delegation de verification · 200 / 401
ℹ Pourquoi /verify est necessaire en microservices

Dans le monolithe, isAuthenticated appelait jwt.verify() directement car JWT_SECRET etait disponible dans le meme processus. En microservices, JWT_SECRET ne quitte jamais auth-service. Les autres services envoient le token a GET /auth/verify — auth-service verifie et retourne le payload decode.

javascriptauth-service/routes/auth.js — /verify
// GET /auth/verify — appele par les autres services, pas par le client
router.get('/verify', function(req, res) {
  const header = req.headers.authorization;
  if (!header) return res.status(401).json({ erreur: 'Token manquant' });

  const token = header.split(' ')[1];
  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);
    res.status(200).json({ userId: payload.userId, email: payload.email, role: payload.role });
  } catch (err) {
    res.status(401).json({ erreur: err.name === 'TokenExpiredError' ? 'Token expire' : 'Token invalide' });
  }
});

2
Appels inter-services avec axios
axios.get · timeout · erreur.response vs erreur.code
javascriptcommande-service/routes/commandes.js — verification token
const axios = require('axios');

async function verifierToken(token) {
  try {
    const reponse = await axios.get('http://localhost:3001/auth/verify', {
      headers:  { Authorization: `Bearer ${token}` },
      timeout:  5000  // 5 secondes max — eviter d'attendre indefiniment
    });
    return reponse.data; // { userId, email, role }

  } catch (err) {
    if (err.response) {
      // Le serveur a repondu avec une erreur HTTP (401, 403...)
      throw { statut: err.response.status, message: err.response.data.erreur };
    } else {
      // ECONNREFUSED = service arrete, ECONNABORTED = timeout
      throw { statut: 503, message: 'auth-service indisponible' };
    }
  }
}
Propriete axiosQuand presenteExemple
err.responseLe serveur a repondu avec un code d'erreurauth-service retourne 401 — token invalide
err.codeLa requete n'a pas atteint le serveurECONNREFUSED (service arrete), ECONNABORTED (timeout)

3
commande-service — POST /commandes
Orchestration · Denormalisation · Coherence
javascriptcommande-service/routes/commandes.js — POST
router.post('/', async function(req, res) {
  // Etape 1 : verifier le token via auth-service
  const token      = req.headers.authorization?.split(' ')[1];
  const utilisateur = await verifierToken(token); // Lance exception si invalide

  const { produitId, quantite } = req.body;

  // Etape 2 : verifier le produit et le stock via produit-service
  const repProduit = await axios.get(`http://localhost:3002/produits/${produitId}`, { timeout: 5000 });
  const produit    = repProduit.data.produit;

  if (!produit.disponible) {
    return res.status(409).json({ erreur: 'Produit hors stock' });
  }

  // Etape 3 : creer la commande avec denormalisation
  // On copie nom et prix du produit — si le produit change, la commande reste coherente
  const commande = {
    userId:      utilisateur.userId,
    produitId:   produitId,
    nomProduit:  produit.nom,    // Denormalisation
    prixUnitaire:produit.prix,   // Denormalisation
    quantite:    quantite,
    total:       produit.prix * quantite,
    statut:      'en_attente',
    createdAt:   new Date()
  };

  const resultat = await getBase().collection('commande').insertOne(commande);
  res.status(201).json({ message: 'Commande creee', commandeId: resultat.insertedId });
});
Pourquoi copier nomProduit et prixUnitaire dans la commande au lieu de les lire depuis produit-service a chaque consultation ?
Partie 3 — Microservices shared · DRY · authClient
⏱ 14 min

Middleware partage isAuthenticated

Module shared/authClient.js (DRY), isAuthenticated et isAdmin centralises, import par chemin relatif, protection des routes produit-service, gestion de la panne auth-service (503).

✓ Ce que vous allez apprendre
  • Module partage shared/authClient.js — centraliser verifierToken()
  • Integrer authClient dans commande-service et produit-service
  • Proteger produit-service : ecriture pour users, suppression pour admins
  • Gerer la panne de auth-service (503) depuis les services dependants

1
Module partage shared/authClient.js
DRY · Chemin relatif · require(../../shared)
ℹ Principe DRY en microservices

DRY (Don't Repeat Yourself) s'applique meme entre services. La fonction verifierToken() dupliquee dans commande-service et produit-service est un probleme : si l'URL d'auth-service change, ou si le timeout passe de 5 a 10 secondes, il faut modifier chaque service. Un module partage shared/authClient.js centralise cette logique — un seul fichier a maintenir.

javascriptshared/authClient.js
const axios = require('axios');

const AUTH_URL = process.env.AUTH_SERVICE_URL || 'http://localhost:3001';

async function verifierToken(token) {
  try {
    const rep = await axios.get(`${AUTH_URL}/auth/verify`, {
      headers: { Authorization: `Bearer ${token}` },
      timeout: 5000
    });
    return rep.data;
  } catch (err) {
    if (err.response) throw { statut: err.response.status, message: err.response.data.erreur };
    throw { statut: 503, message: 'auth-service indisponible' };
  }
}

// Middleware isAuthenticated partage entre tous les services
function isAuthenticated(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ erreur: 'Token manquant' });

  verifierToken(token)
    .then(function(payload) {
      req.utilisateur = payload;
      next();
    })
    .catch(function(err) {
      res.status(err.statut || 401).json({ erreur: err.message });
    });
}

// Middleware isAdmin — utiliser apres isAuthenticated
function isAdmin(req, res, next) {
  if (!req.utilisateur || req.utilisateur.role !== 'admin') {
    return res.status(403).json({ erreur: 'Acces admin requis' });
  }
  next();
}

module.exports = { verifierToken, isAuthenticated, isAdmin };
javascriptproduit-service/routes/produits.js — import shared
// Chemin relatif depuis produit-service/ vers shared/
const { isAuthenticated, isAdmin } = require('../../shared/authClient');

// GET public
router.get('/', async function(req, res) { /* ... */ });

// POST protege — user connecte requis
router.post('/', isAuthenticated, async function(req, res) { /* ... */ });

// DELETE — admin uniquement
router.delete('/:id', isAuthenticated, isAdmin, async function(req, res) { /* ... */ });

✓
Verification des acquis
3 questions
commande-service appelle isAuthenticated. auth-service est arrete. Quel code HTTP recoit le client ?
Peut-on utiliser isAdmin sans isAuthenticated avant lui ?
Partie 3 — Microservices RabbitMQ · exchange · queue
⏱ 15 min

RabbitMQ — Concepts et installation

Limites de la communication synchrone pour les actions secondaires, concepts RabbitMQ (producer, exchange, queue, consumer, ack), types d'exchange direct/fanout/topic, installation avec Docker.

✓ Ce que vous allez apprendre
  • Probleme de la communication synchrone pour les actions secondaires
  • Concepts RabbitMQ : producer, broker, exchange, queue, consumer
  • Acknowledgement — garantie de traitement
  • Types d'exchange : direct, fanout, topic
  • Interface web RabbitMQ (port 15672)

1
Probleme du synchrone pour les actions secondaires
Couplage temporal · Cascade de pannes · Latence

Quand une commande est creee, 4 actions doivent se declencher : decrement stock, email de confirmation, generation facture, enregistrement analytics. Deux approches synchrones, deux problemes :

  • Sequentiel : 4 appels axios sequentiels = 4 x 200ms = 800ms minimum. Si analytics-service prend 5 secondes, le client attend 5 secondes pour creer une commande.
  • Parallele (Promise.all) : si notif-service est en panne, Promise.all echoue et la commande n'est pas creee — l'utilisateur recoit une erreur a cause d'un email.

Solution : commande-service publie un evenement "commande.creee" et repond immediatement au client. Les services interesses traitent l'evenement quand ils sont disponibles.


2
Concepts RabbitMQ
Producer · Exchange · Queue · Consumer · Acknowledgement
ConceptRoleAnalogie
ProducerPublie des messages vers l'exchangeL'expediteur d'un colis
ExchangeRecoit les messages et les route vers les bonnes queues selon les reglesLe centre de tri postal
QueueFile d'attente FIFO — stocke les messages jusqu'au traitementLa boite aux lettres du destinataire
ConsumerS'abonne a une queue et traite les messagesLe destinataire qui vide sa boite
AcknowledgementConfirmation que le message a ete traite — RabbitMQ le supprime de la queueSigner le bon de reception
BrokerLe serveur RabbitMQ qui gere exchanges et queuesLa poste entiere
ℹ Sans acknowledgement — risque de perte

Si le consumer traite un message et plante avant d'envoyer l'ack, RabbitMQ relivrera le message a un autre consumer. Sans ack, RabbitMQ suppose que le message n'a pas ete traite et le relivrera indefiniment — mais avec ack, la garantie at-least-once est assuree.


3
Types d'exchange
Direct · Fanout · Topic
TypeRoutageCas d'usage microservices
DirectCle de routage exacte — un message vers une queue preciseDecrement stock apres commande — seul stock-service recoit
FanoutBroadcast — toutes les queues liees recoivent le messageNotifier tous les services d'un evenement systeme (maintenance)
TopicPattern avec wildcards (* et #)commande.* — stock-service et notif-service s'abonnent chacun
bashInstallation RabbitMQ
# Avec Docker (recommande)
docker run -d --name rabbitmq   -p 5672:5672   -p 15672:15672   rabbitmq:3-management

# Interface web : http://localhost:15672
# Identifiants par defaut : guest / guest
Quel type d'exchange utiliser pour qu'un evenement "commande.creee" soit recu a la fois par stock-service ET notif-service ?
Partie 3 — Microservices amqplib · producer · consumer · ack
⏱ 18 min

amqplib — Producer, Consumer, Integration

4 etapes obligatoires amqplib, producer.js (Buffer + publish), consumer.js (bindQueue + consume + ack/nack), resilience des queues, module partage shared/rabbitmq.js avec reconnexion automatique, integration dans commande-service.

✓ Ce que vous allez apprendre
  • Les 4 étapes obligatoires amqplib : connect, createChannel, assertExchange, assertQueue
  • producer.js : publier un message JSON dans RabbitMQ
  • consumer.js : s'abonner, recevoir, ack() et nack()
  • Module partage shared/rabbitmq.js avec reconnexion automatique
  • Integrer la publication dans commande-service et tester notif-service

1
Les 4 etapes obligatoires amqplib
connect · createChannel · assertExchange · assertQueue
ℹ Pourquoi 4 etapes et pas 1 ?

Chaque etape a un role distinct et peut echouer independamment. Connexion = tunnel TCP avec le broker. Channel = canal logique multiplex (une connexion peut en avoir plusieurs). assertExchange = verifier/creer l'exchange (idempotent). assertQueue = verifier/creer la queue, puis la lier a l'exchange (cote consumer uniquement).

EtapeMethode amqplibRoleProducer ou Consumer ?
1amqplib.connect(url)Connexion TCP au broker RabbitMQLes deux
2connexion.createChannel()Canal logique sur la connexion (moins couteux qu'une nouvelle connexion)Les deux
3channel.assertExchange(nom, type, opts)Declarer l'exchange — le creer s'il n'existe pas (idempotent)Les deux
4achannel.publish(exchange, key, buffer)Publier un messageProducer uniquement
4bchannel.assertQueue(nom, opts)Declarer la queueConsumer uniquement
4cchannel.bindQueue(queue, exchange, key)Lier la queue a l'exchangeConsumer uniquement
4dchannel.consume(queue, callback)S'abonner et recevoir les messagesConsumer uniquement
ackchannel.ack(msg)Confirmer le traitement — RabbitMQ supprime le messageConsumer uniquement
bashterminal
mkdir app-microservices/test-amqp && cd app-microservices/test-amqp
npm init -y
npm install amqplib   # Bibliotheque officielle AMQP 0-9-1 pour Node.js

2
producer.js — publier un message
Buffer.from · JSON.stringify · persistent
⚠ Message perdu si aucune queue n'est liee

RabbitMQ route les messages d'un exchange vers les queues liees (bindings). Si aucune queue n'est liee au moment de la publication, le message est ignore et perdu. Ordre correct : les consumers (qui creent les queues) doivent demarrer avant ou en meme temps que les producers.

javascripttest-amqp/producer.js
const amqplib = require('amqplib');

const RABBITMQ_URL  = 'amqp://guest:guest@localhost:5672';
const EXCHANGE_NOM  = 'commandes';
const EXCHANGE_TYPE = 'fanout'; // Broadcast : tous les consumers abonnes recoivent le message

async function publier(evenement) {
  let connexion;
  try {
    connexion = await amqplib.connect(RABBITMQ_URL);           // Etape 1 : connexion TCP
    const channel = await connexion.createChannel();           // Etape 2 : canal logique

    await channel.assertExchange(                              // Etape 3 : declarer l'exchange
      EXCHANGE_NOM, EXCHANGE_TYPE, { durable: true }          // durable: true = survit au redemarrage
    );

    const buffer = Buffer.from(JSON.stringify(evenement));    // amqplib envoie des Buffer
    channel.publish(EXCHANGE_NOM, '', buffer, {
      persistent: true   // Message ecrit sur disque — survit au redemarrage RabbitMQ
    });
    console.log('Message publie :', JSON.stringify(evenement));

    await channel.close();
    await connexion.close();
  } catch (e) {
    console.error('Erreur RabbitMQ :', e.message);
    if (connexion) await connexion.close().catch(() => {});
    process.exit(1);
  }
}

publier({
  type:       'commande.creee',
  commandeId: 'cmd-test-001',
  userId:     'user-karim',
  total:      215,
  timestamp:  new Date().toISOString()
});
ℹ Verification dans l'interface web

Apres node producer.js, ouvrir http://localhost:15672 → onglet Exchanges → 'commandes'. L'exchange a ete cree. Aller dans Queues — aucune queue n'est encore liee, le message a ete perdu. C'est normal — le consumer doit etre demarre en premier.


3
consumer.js — s'abonner et traiter les messages
assertQueue · bindQueue · consume · ack · nack
ℹ Difference cle avec le producer

Le consumer ne se ferme pas apres avoir consomme un message — il reste en ecoute indefiniment. Pas de connexion.close() a la fin — le processus reste actif. channel.ack(msg) est obligatoire : sans ack, RabbitMQ garde le message en etat “unacked” et le relivrera apres redemarrage.

javascripttest-amqp/consumer.js
const amqplib = require('amqplib');

const RABBITMQ_URL = 'amqp://guest:guest@localhost:5672';
const EXCHANGE_NOM = 'commandes';
const QUEUE_NOM    = 'queue-notifs';

async function demarrerConsumer() {
  try {
    const connexion = await amqplib.connect(RABBITMQ_URL);
    const channel   = await connexion.createChannel();
    console.log('Consumer connecte a RabbitMQ');

    await channel.assertExchange(EXCHANGE_NOM, 'fanout', { durable: true }); // Idempotent

    await channel.assertQueue(QUEUE_NOM, { durable: true });  // Creer si inexistante
    await channel.bindQueue(QUEUE_NOM, EXCHANGE_NOM, '');     // '' = routing key vide (fanout)
    console.log('En attente de messages dans', QUEUE_NOM);

    channel.consume(QUEUE_NOM, function(msg) {
      if (msg === null) { console.log('Consumer annule'); return; }
      try {
        const contenu = JSON.parse(msg.content.toString()); // msg.content est un Buffer
        console.log('Message recu :', contenu);
        console.log(`Email envoye : commande ${contenu.commandeId} — ${contenu.total} MAD`);
        channel.ack(msg);   // OBLIGATOIRE — confirmer le traitement
      } catch (e) {
        console.error('Erreur traitement :', e.message);
        channel.nack(msg, false, false); // false, false = pas de requeue (evite boucle infinie)
      }
    }, { noAck: false }); // noAck: false = mode manuel — on gere l'ack

    process.on('SIGINT', async () => {
      await channel.close();
      await connexion.close();
      console.log('Consumer arrete proprement');
      process.exit(0);
    });
  } catch (e) {
    console.error('Erreur consumer :', e.message);
    process.exit(1);
  }
}

demarrerConsumer();
✓ Tester la resilience
  • Demarrer le consumer (terminal 1), puis le producer (terminal 2) → message recu immediatement.
  • Arreter le consumer (Ctrl+C), publier 3 messages. Interface web : queue-notifs affiche 3 messages en attente.
  • Relancer le consumer → les 3 messages sont immediatement consommes. Aucun message perdu.

4
Module partage shared/rabbitmq.js + integration commande-service
Singleton · Reconnexion automatique · publierEvenement

La logique de connexion RabbitMQ sera repete dans chaque service qui publie des messages. Meme principe que shared/authClient.js : centraliser pour eviter la duplication. shared/rabbitmq.js expose une connexion singleton avec reconnexion automatique.

javascriptshared/rabbitmq.js
const amqplib = require('amqplib');

const RABBITMQ_URL = process.env.RABBITMQ_URL || 'amqp://guest:guest@localhost:5672';
const EXCHANGE_NOM = process.env.EXCHANGE_NOM  || 'commandes';

let channel = null; // Singleton — une seule connexion partagee

async function connecterRabbitMQ() {
  try {
    const connexion = await amqplib.connect(RABBITMQ_URL);
    channel = await connexion.createChannel();
    await channel.assertExchange(EXCHANGE_NOM, 'fanout', { durable: true });
    console.log('RabbitMQ connecte — exchange:', EXCHANGE_NOM);

    connexion.on('close', () => {
      console.warn('Connexion RabbitMQ perdue — reconnexion dans 5s');
      channel = null;
      setTimeout(connecterRabbitMQ, 5000); // Reconnexion automatique
    });
  } catch (e) {
    console.warn('RabbitMQ indisponible :', e.message, '— retry dans 5s');
    setTimeout(connecterRabbitMQ, 5000);
  }
}

async function publierEvenement(evenement) {
  if (!channel) {
    console.warn('RabbitMQ indisponible — evenement non publie:', evenement.type);
    return false; // Ne pas bloquer la creation de commande
  }
  try {
    const buffer = Buffer.from(JSON.stringify(evenement));
    channel.publish(EXCHANGE_NOM, '', buffer, { persistent: true });
    return true;
  } catch (e) {
    console.warn('Erreur publication RabbitMQ :', e.message);
    return false;
  }
}

module.exports = { connecterRabbitMQ, publierEvenement, EXCHANGE_NOM };
✓ Integration dans commande-service — apres insertOne()
const { publierEvenement } = require('../../shared/rabbitmq');

// Apres insertOne() dans la route POST /commandes :
const evenement = {
  type:       'commande.creee',
  commandeId: r.insertedId.toString(),
  userId:     utilisateur.userId,
  total:      total,
  timestamp:  new Date().toISOString()
};
const publie = await publierEvenement(evenement);
// Repondre IMMEDIATEMENT au client — sans attendre le consumer
res.status(201).json({ message: 'Commande creee', commande: { _id: r.insertedId } });

✓
Tableau a completer — 4 etapes amqplib
Synthese · Producer vs Consumer

Completer le tableau sur le cahier sans consulter les notes. Durée : 3 minutes.

EtapeMethode amqplibRoleQui ?
1amqplib.___(url)Connexion TCP au brokerLes deux
2connexion.___() Canal logiqueLes deux
3channel.assertExchange()Declarer l'exchangeLes deux
4achannel.___(exchange, key, buffer)Envoyer un messageProducer
4bchannel.assertQueue()Declarer la queueConsumer
4cchannel.___(queue, exchange, key)Lier queue a exchangeConsumer
4dchannel.___(queue, callback)S'abonnerConsumer
ackchannel.___(msg)Confirmer traitementConsumer
✓ Corrige

connect, createChannel, publish, bindQueue, consume, ack

Un consumer traite un message et plante avant d'envoyer l'ack. Que fait RabbitMQ ?
Dans shared/rabbitmq.js, publierEvenement() retourne false si RabbitMQ est indisponible plutot que de lever une exception. Pourquoi ?
Partie 3 — Microservices Nginx · proxy_pass · upstream
⏱ 16 min

Nginx reverse proxy + TP consolidation Partie 3

Forward vs reverse proxy, fonctions Nginx (routage, SSL, rate limiting), comparaison Gateway Express vs Nginx, ecriture de nginx.conf complet pour les 4 services, TP 15 scenarios flux complet + tests de tolerance aux pannes.

✓ Ce que vous allez apprendre
  • Forward proxy vs reverse proxy — direction de la protection
  • Fonctions Nginx : routage, SSL/TLS, load balancing, rate limiting, compression, logs
  • Nginx vs Gateway Express — comparaison sur 5 criteres
  • Ecrire et deployer nginx.conf pour les 4 services
  • TP consolidation Partie 3 : 15 scenarios flux complet + tests de tolerance aux pannes

1
Forward proxy vs Reverse proxy
Direction de protection · Topologie masquee · Fonctions Nginx
TypeQui est protege ?Qui ne voit pas quoi ?Exemple
Forward proxyLe clientLe serveur ne voit pas l'IP reelle du clientVPN d'entreprise, proxy scolaire
Reverse proxyLe serveurLe client ne connait pas l'adresse des backendsNginx devant les microservices, Cloudflare

Dans notre architecture : Client → Nginx (port 80) → [auth:3001 | produit:3002 | commande:3003]. Le client ne sait pas que 3 services Node.js distincts existent. Nginx masque completement la topologie interne.

Fonctions d'un reverse proxy Nginx

  • Routage (proxy_pass) : rediriger /auth/* vers auth-service, /produits/* vers produit-service.
  • SSL/TLS termination : le client parle HTTPS avec Nginx, Nginx parle HTTP avec les services internes (3 lignes dans nginx.conf).
  • Rate limiting : limit_req_zone — limiter les tentatives brute force sur /auth/login.
  • Compression gzip : compresser les reponses JSON pour reduire la bande passante.
  • Logging centralise : toutes les requetes dans un format uniforme sans modifier le code.
  • Headers de securite : X-Frame-Options, X-Content-Type-Options sans toucher a Node.js.
CritereGateway Express (Node.js)Nginx
Performance~50 Mo RAM, event loop Node.js~2 Mo RAM, C natif — 50 000+ connexions simultanees
ConfigurationCode JavaScript — require, app.use()Fichier nginx.conf declaratif — pas de code
Logique metierPeut executer du JS (JWT, transformation)Pas de logique metier — routage pur
SSL/TLSVerbeux (https.createServer + cert)Natif — 3 lignes dans nginx.conf
Cas d'usage idealPrototype, logique de routage complexeProduction, haute charge, SSL, rate limiting

2
Installation et configuration nginx.conf
proxy_pass · upstream · location · proxy_set_header
bashInstallation Nginx
# Ubuntu / Debian
sudo apt update && sudo apt install nginx -y
sudo systemctl start nginx

# macOS (Homebrew)
brew install nginx && brew services start nginx

# Verification (tous systemes) : http://localhost → "Welcome to nginx!"

# Tester la syntaxe du fichier (TOUJOURS avant reload)
sudo nginx -t
# Recharger sans redemarrer (zero downtime)
sudo nginx -s reload
nginxapp-microservices/nginx/nginx.conf
events {
  worker_connections 1024;  # Connexions simultanees par worker process
}

http {
  # Definir les groupes de serveurs backend
  upstream auth_service     { server localhost:3001; }
  upstream produit_service  { server localhost:3002; }
  upstream commande_service { server localhost:3003; }

  server {
    listen 80;
    server_name localhost;

    # Headers transmis aux services backend
    proxy_set_header Host            $host;
    proxy_set_header X-Real-IP       $remote_addr;
    proxy_set_header Authorization   $http_authorization;  # CRITIQUE : transmettre le token JWT

    proxy_connect_timeout  5s;
    proxy_read_timeout    30s;

    location /auth/     { proxy_pass http://auth_service/auth/; }
    location /produits/ { proxy_pass http://produit_service/produits/; }
    location /commandes/{ proxy_pass http://commande_service/commandes/; }

    location /nginx-health {
      return 200 '{"service":"nginx","statut":"actif"}';
      add_header Content-Type application/json;
    }

    # Page d'erreur si service Node.js en panne
    error_page 502 503 504 /erreur_service.json;
    location = /erreur_service.json {
      return 503 '{"erreur":"Service temporairement indisponible"}';
      add_header Content-Type application/json;
    }
  }
}
ℹ BONUS — Rate limiting sur /auth/login
# Dans le bloc http{}, avant server{} :
limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=10r/m;

# Dans location /auth/ :
limit_req zone=auth_limit burst=5 nodelay;
# Resultat : 429 Too Many Requests apres 5 tentatives en rafale

3
TP consolidation Partie 3 — flux complet
15 scenarios Postman · Tolerance aux pannes · README

Tous les appels se font sur http://localhost (port 80 — Nginx). Aucun appel direct sur les ports 3001-3003.

Etape 1 — Demarrage (5 terminaux)

  • Terminal 1 : cd auth-service && node index.js → port 3001
  • Terminal 2 : cd produit-service && node index.js → port 3002
  • Terminal 3 : cd commande-service && node index.js → port 3003
  • Terminal 4 : cd notif-service && node index.js → consumer RabbitMQ
  • Nginx : actif en tant que service systeme
#RequeteTokenAttendu
1POST /auth/register nadia@artisan.ma—201
2POST /auth/login nadia → token_nadia—200 + token
3GET /auth/verifytoken_nadia200 + { role: 'user' }
4GET /produitsaucun200 (route publique)
5POST /produits (creer huile argan)token_nadia201 — copier l'_id
6DELETE /produits/{id}token_nadia403 (user ne peut pas supprimer)
7POST /commandes (1 produit)token_nadia201 + notification terminal notif-service
8POST /commandes (produit stock=0)token_nadia409 Conflict
9POST /commandes (sans token)—401
10GET /commandestoken_nadia200 + commandes de nadia

Tests de tolerance aux pannes

  • Arreter produit-service → POST /commandes : 503 — “produit-service indisponible”. GET /produits via Nginx : 503 (error_page 502). Relancer produit-service.
  • Arreter notif-service → POST /commandes : 201 quand meme. Interface RabbitMQ : 1 message en attente. Relancer notif-service → message consomme immediatement.
  • Arreter RabbitMQ → POST /commandes : 201 (publierEvenement() retourne false). Observer le log “RabbitMQ indisponible” dans commande-service. Relancer RabbitMQ → reconnexion automatique apres 5s.
notif-service est arrete pendant 2 heures. Durant ce temps, 50 commandes sont creees. Que se passe-t-il au redemarrage de notif-service ?
Dans nginx.conf, pourquoi proxy_set_header Authorization $http_authorization est-il critique ?
Partie 4 — Docker Isolation · Legerete · Portabilite
⏱ 15 min

Notion de conteneurs — Isolation, legerete, portabilite

Causes du probleme "ca marche chez moi", 3 proprietes fondamentales (isolation namespace, legerete kernel partage, portabilite build-once-run-anywhere), VM vs conteneur sur 6 criteres, 5 avantages Cloud Native, ecosysteme Docker/Kubernetes.

✓ Ce que vous allez apprendre
  • Les causes du probleme "ca marche chez moi" — 4 categories
  • Les 3 proprietes fondamentales d'un conteneur : isolation, legerete, portabilite
  • Machine virtuelle vs conteneur — comparaison sur 6 criteres
  • 5 avantages des conteneurs pour les applications Cloud Native
  • Ecosysteme : Docker, Podman, containerd, Kubernetes

1
Le probleme "ca marche chez moi"
Environnement · Configuration · Dependances
ℹ Les 4 categories de causes
  • Environnement d'execution : version runtime differente (Node.js 16 vs 20), OS different (Windows vs Linux — chemins, separateurs), bibliotheques systeme differentes.
  • Configuration : variables d'environnement absentes (.env non copie), ports deja utilises, permissions de fichiers differentes.
  • Dependances : versions npm divergentes (package-lock.json ignore), modules natifs compiles pour un OS, dependances systeme absentes (MongoDB, RabbitMQ).
  • Donnees : base de donnees vide en production vs peuplee en developpement, fichiers temporaires influencant le comportement.
ℹ La solution historique : les VMs — et pourquoi insuffisante

Avant les conteneurs, on deployait des machines virtuelles completes. Avantage : isolation totale. Probleme : une VM pese plusieurs Go (OS inclus) et met plusieurs minutes a demarrer. Les conteneurs apportent les memes garanties d'isolation avec une empreinte 10 a 100 fois plus legere.


2
Les 3 proprietes fondamentales
Isolation namespace · Legerete kernel · Portabilite build-once
ℹ Definition formelle

Un conteneur est une unite d'execution legere et isolee qui empaquete une application avec toutes ses dependances dans un seul artefact portable. Il partage le kernel de l'OS hote mais s'execute dans un espace de noms (namespace) isole.

Isolation — namespaces Linux

Namespace PID (voit uniquement ses processus), namespace reseau (interface propre), namespace fichiers (systeme de fichiers propre). Analogie : deux appartements dans le meme immeuble. L'immeuble (machine physique) est partage, mais chaque appartement (conteneur) a son espace prive. Les voisins partagent l'ascenseur (le kernel) mais ne peuvent pas entrer chez vous.

Legerete — kernel partage

VM : OS complet embarque → 5-20 Go, demarrage 1-5 min. Conteneur : partage le kernel de l'hote → 50-500 Mo, demarrage < 1 seconde. Pour l'application Partie 3 : 4 VM × ~10 Go = ~40 Go. Avec conteneurs : image node:18-alpine (~50 Mo) partagee = ~300 Mo total.

Portabilite — build once, run anywhere

Une image construite une fois tourne identiquement sur le MacBook du developpeur, le serveur Ubuntu, le pipeline CI/CD et Azure/AWS. Analogie : le conteneur maritime ISO 668 — meme boite, n'importe quel port mondial peut la manipuler.


3
Machine virtuelle vs conteneur — 6 criteres
Taille · Demarrage · Isolation · OS · Performance · Cas d'usage
CritereMachine VirtuelleConteneur Docker
Taille5–20 Go (OS complet)50–500 Mo (kernel partage)
Demarrage1–5 minutes (boot OS)< 1 seconde (processus)
IsolationTotale — kernel propre, hyperviseurForte — namespaces, kernel partage
OSOS complet par VMPas d'OS — kernel Linux partage
Performance5–15% overhead (hyperviseur)< 1% overhead (namespaces natifs)
Cas d'usageIsolation maximale, OS differents, legacyMicroservices, CI/CD, Cloud Native
⚠ Conteneurs et kernel Linux

Sur Windows et macOS, Docker Desktop cree une VM Linux legere (WSL2) transparente pour le developpeur. En production sur un serveur Linux : conteneurs directement sur le kernel — aucune VM intermediaire.


4
5 avantages Cloud Native + ecosysteme
Reproductibilite · Scalabilite · CI/CD · IaC · Podman · Kubernetes
  • Reproductibilite : le Dockerfile precise FROM node:18.17.0-alpine — cette version exacte partout. Bug "ca marche chez moi" = impossible.
  • Isolation des versions : deux services peuvent utiliser des versions differentes de bcryptjs sans conflit — chacun a son propre systeme de fichiers.
  • Scalabilite horizontale : docker-compose scale commande-service=10 → 10 instances identiques en secondes.
  • CI/CD automatise : GitHub Actions construit, pousse sur Docker Hub, deploie — sans intervention humaine. Rollback en une commande.
  • Infrastructure as Code : un fichier docker-compose.yml decrit toute l'architecture. Nouveau developpeur : git clone + docker-compose up → application complete en < 5 minutes.
OutilRoleDans ce module
DockerCreer, executer, distribuer des conteneursSeances 25–29 — outil principal
Docker ComposeOrchestrer des architectures multi-conteneursSeances 28–29
PodmanAlternative Docker sans daemon rootNon couvert — memes concepts
KubernetesOrchestrateur grande echelle (scaling, auto-healing)Non couvert dans ce module
L'equipe utilise bcryptjs 2.4.3. Cette version est retiree de npm dans 6 mois. Le conteneur fonctionnera-t-il toujours ?
Partie 4 — Docker docker run · pull · exec · logs
⏱ 17 min

Docker — Installation, terminologie et commandes de base

Installation Docker Desktop, hello-world (cycle complet), 6 concepts (Image/Conteneur/Registry/Tag/Layer/Hub), 12 manipulations cycle de vie, 5 options docker run (-p -e -d --name -v), host.docker.internal, lancer auth-service dans un conteneur.

✓ Ce que vous allez apprendre
  • Installer Docker Desktop et verifier avec docker run hello-world
  • 6 concepts fondamentaux : Image, Conteneur, Registry, Tag, Layer, Docker Hub
  • Cycle de vie complet en 12 manipulations guidees
  • 5 options essentielles de docker run : -p, -e, -d, --name, -v
  • Lancer auth-service dans un conteneur avec montage du code source

1
Installation et premier conteneur
Docker Desktop · hello-world · Cycle pull-run
bashInstallation et verification
# Telecharger : https://www.docker.com/products/docker-desktop/
# Windows : installer avec WSL2 active, redemarrer
# macOS   : glisser Docker.app dans Applications
# Linux   : sudo apt install docker.io docker-compose -y

docker version          # Versions Client et Server
docker info             # Configuration complete du daemon
docker run hello-world  # Premier conteneur — test complet
ℹ Ce que fait docker run hello-world en 5 etapes
  1. Cherche l'image 'hello-world' localement → introuvable
  2. Docker pull : telecharge depuis Docker Hub
  3. Docker create : cree un conteneur depuis l'image
  4. Docker start : demarre le conteneur
  5. Le conteneur affiche 'Hello from Docker!' et s'arrete

2
6 concepts fondamentaux Docker
Image · Conteneur · Registry · Tag · Layer · Docker Hub
ConceptDefinitionExemple
ImageSnapshot immuable : code + runtime + dependances. Analogie : classe POO, moule a gateau.node:18-alpine, mongo:7.0
ConteneurInstance en cours d'execution d'une image. Analogie : objet instancie depuis une classe.docker run node:18-alpine
RegistryServeur de stockage d'images. Public : Docker Hub. Prive : Azure Container Registry.hub.docker.com
TagVersion d'une image. latest = non deterministe en production — a eviter.node:18-alpine, node:20-slim
LayerCouche du systeme de fichiers. Chaque instruction Dockerfile = une couche. Partagees entre images.docker image history node:18-alpine
Docker HubRegistry public officiel avec les images officielles node, mongo, nginx, rabbitmq...hub.docker.com/_/node
ℹ Pourquoi node:18-alpine et pas node:18 ?

node:18 (Debian) ≈ 1 Go. node:18-slim ≈ 250 Mo. node:18-alpine ≈ 50 Mo. Alpine Linux est une distribution minimaliste (~5 Mo) concue pour les conteneurs. Tous les packages de ce module (Express, Mongoose, Axios, amqplib) sont compatibles Alpine.


3
Cycle de vie complet — 12 manipulations
pull · run · ps · exec · logs · stop · start · rm · inspect · stats · cp · prune
bash12 manipulations guidees
# 1 — Conteneur interactif
docker run -it node:18-alpine sh      # -it = interactif + TTY
# Dans le conteneur : node --version, ls /, exit

# 2 — Conteneur en arriere-plan
docker run -d --name mon-node node:18-alpine sleep 3600

# 3 — Lister les conteneurs
docker ps          # Actifs uniquement
docker ps -a       # Tous (actifs + arretes)

# 4 — Executer dans un conteneur actif
docker exec mon-node node --version
docker exec -it mon-node sh

# 5 — Logs
docker logs mon-node
docker logs -f mon-node   # Temps reel (Ctrl+C)

# 6 — Arreter et redemarrer
docker stop mon-node      # SIGTERM → attendre → SIGKILL
docker start mon-node

# 7 — Inspecter
docker inspect mon-node
docker inspect mon-node | grep IPAddress

# 8 — Supprimer
docker rm mon-node         # Doit etre arrete
docker rm -f mon-node      # Force (arret + suppression)

# 9 — Gerer les images
docker images              # Lister les images locales
docker rmi hello-world     # Supprimer une image
docker image prune         # Supprimer les images non utilisees

# 10 — Statistiques temps reel
docker run -d --name stats-test node:18-alpine sleep 3600
docker stats stats-test    # CPU%, RAM, reseau (Ctrl+C)
docker stop stats-test && docker rm stats-test

# 11 — Copier des fichiers
docker run -d --name copy-test node:18-alpine sleep 3600
echo "console.log('Bonjour')" > test.js
docker cp test.js copy-test:/tmp/test.js  # local → conteneur
docker exec copy-test node /tmp/test.js
docker stop copy-test && docker rm copy-test

# 12 — Nettoyage
docker system prune      # Conteneurs arretes + images non utilisees
docker system df         # Espace utilise par Docker

4
5 options essentielles de docker run + auth-service
-p · -e · -d · --name · -v · host.docker.internal
OptionRoleExemple
-p HOST:CONTMapper un port hote vers le conteneur. Sans -p : inaccessible depuis l'exterieur.-p 3001:3001
-e CLE=VALVariable d'environnement. Remplace le fichier .env.-e JWT_SECRET=secret
-dDetached : lancer en arriere-plan.docker run -d ...
--name NOMNommer le conteneur (sinon nom aleatoire).--name auth-service
-v HOTE:CONTMonter un dossier hote dans le conteneur. Hot reload en dev, persistance de donnees.-v $(pwd)/auth:/app
bashLancer auth-service dans un conteneur
cd app-microservices
cd auth-service && npm install && cd ..

docker run -d \
  --name auth-service \
  -p 3001:3001 \
  -e PORT=3001 \
  -e MONGO_URI=mongodb://host.docker.internal:27017 \
  -e NOM_BASE=bdms-auth \
  -e JWT_SECRET=secret_dev_123 \
  -e JWT_EXPIRES_IN=24h \
  -v $(pwd)/auth-service:/app \
  -w /app \
  node:18-alpine \
  node index.js

docker ps                     # Verifier
docker logs auth-service      # Observer les logs
docker logs -f auth-service   # Temps reel
ℹ host.docker.internal — acceder a l'hote depuis le conteneur

Depuis un conteneur, localhost designe le conteneur lui-meme. Pour acceder a MongoDB ou RabbitMQ sur l'hote : host.docker.internal (Windows/macOS automatique, Linux : --add-host=host.docker.internal:host-gateway). Avec Docker Compose : chaque service se reference par son nom de service (mongodb://mongo:27017).


✓
Evaluation — 5 scenarios docker run
Ecrire la commande exacte

Ecrire chaque commande docker run complete sur le cahier. Duree : 4 minutes.

  1. Lancer produit-service en arriere-plan, port 3002, variables PORT=3002 et MONGO_URI=mongodb://host.docker.internal:27017, code monte depuis ./produit-service vers /app, image node:18-alpine, commande : node index.js.
  2. Ouvrir un shell interactif dans le conteneur 'produit-service'.
  3. Lister tous les conteneurs (actifs ET arretes).
  4. Arreter et supprimer 'auth-service' en une seule commande.
  5. Lancer MongoDB en arriere-plan, port 27017, variables MONGO_INITDB_ROOT_USERNAME=admin et MONGO_INITDB_ROOT_PASSWORD=secret, image mongo:7.0.
✓ Corrige
  • Sc 1 : docker run -d --name produit-service -p 3002:3002 -e PORT=3002 -e MONGO_URI=mongodb://host.docker.internal:27017 -v $(pwd)/produit-service:/app -w /app node:18-alpine node index.js
  • Sc 2 : docker exec -it produit-service sh
  • Sc 3 : docker ps -a
  • Sc 4 : docker rm -f auth-service
  • Sc 5 : docker run -d --name mongodb -p 27017:27017 -e MONGO_INITDB_ROOT_USERNAME=admin -e MONGO_INITDB_ROOT_PASSWORD=secret mongo:7.0
Sans l'option -p 3001:3001, que se passe-t-il quand on accede a http://localhost:3001 ?
Partie 4 — Docker Dockerfile · docker build · .dockerignore
⏱ 17 min

Dockerfile — Ecrire, construire et optimiser

7 instructions (FROM WORKDIR COPY RUN ENV EXPOSE CMD), Dockerfile complet de auth-service, docker build avec observation des Steps et du cache, .dockerignore (exclure node_modules), ordre optimal des instructions, TP Dockerfile produit-service + 10 tests.

✓ Ce que vous allez apprendre
  • Les 7 instructions Dockerfile essentielles : FROM, WORKDIR, COPY, RUN, ENV, EXPOSE, CMD
  • Ecrire le Dockerfile complet de auth-service + docker build -t nom:tag .
  • L'ordre optimal des instructions pour le cache Docker (npm install en cache)
  • Creer .dockerignore pour exclure node_modules du contexte de build
  • TP : Dockerfile de produit-service + 10 tests de validation Postman

1
Les 7 instructions Dockerfile
FROM · WORKDIR · COPY · RUN · ENV · EXPOSE · CMD
ℹ Qu'est-ce qu'un Dockerfile ?

Un Dockerfile est un fichier texte (sans extension) qui decrit, etape par etape, comment construire une image Docker. Chaque instruction cree une couche (layer). Docker lit le Dockerfile de haut en bas — l'ordre a une importance critique pour les performances (cache). Analogie : une recette de cuisine. La recette = le Dockerfile, les ingredients = l'image de base + le code, le plat fini = l'image construite, une portion servie = un conteneur lance.

InstructionRoleDetail important
FROM node:18-alpineImage de base — premiere instruction obligatoirealpine = ~50 Mo vs ~1 Go pour node:18
WORKDIR /appDefinir le repertoire de travail dans le conteneurCree /app si inexistant. Sans WORKDIR : fichiers dans / (racine) — mauvaise pratique
COPY package*.json ./Copier les manifestes de dependancesLe glob * capture package.json ET package-lock.json. Destination ./ = /app/ (WORKDIR actif)
RUN npm install --productionExecuter une commande pendant le BUILD--production : n'installe pas les devDependencies → image plus legere. Cree une couche node_modules
ENV NODE_ENV=productionVariable d'environnement par defautAccessible via process.env. Surchargeble avec docker run -e
EXPOSE 3001Documenter le port d'ecouteN'OUVRE PAS le port — informatif. Le mapping se fait avec -p
CMD ["node", "index.js"]Commande de demarrage par defautExec form (tableau) recommandee — evite un shell intermediaire. Une seule CMD par Dockerfile

2
Dockerfile de auth-service + docker build + docker run
docker build -t · Observation des Steps · docker image history
dockerfileauth-service/Dockerfile
# ── Etape 1 : Image de base ─────────────────────────────────
FROM node:18-alpine              # Alpine = ~50 Mo vs ~1 Go pour node:18

# ── Etape 2 : Repertoire de travail ─────────────────────────
WORKDIR /app                     # Cree /app et s'y positionne

# ── Etape 3 : Copier les manifestes de dependances ──────────
COPY package*.json ./            # package.json + package-lock.json

# ── Etape 4 : Installer les dependances ─────────────────────
RUN npm install --production     # Sans devDependencies — image plus legere

# ── Etape 5 : Copier le code source ─────────────────────────
COPY . .                         # Copier tout le reste dans /app

# ── Etape 6 : Variables d'environnement par defaut ──────────
ENV NODE_ENV=production
ENV PORT=3001

# ── Etape 7 : Port documente ─────────────────────────────────
EXPOSE 3001                      # Informatif — le mapping se fait avec -p

# ── Etape 8 : Commande de demarrage ─────────────────────────
CMD ["node", "index.js"]         # Exec form — sans shell intermediaire
bashConstruire et lancer
cd app-microservices/auth-service

# Construire l'image — observer chaque Step dans la sortie
docker build -t bdms-auth:1.0.0 .
# Step 1/8 : FROM node:18-alpine   → telecharge ou utilise le cache
# Step 4/8 : RUN npm install       → installe les dependances (long la 1ere fois)
# Step 5/8 : COPY . .              → copie le code source

docker images | grep bdms-auth        # Verifier la taille de l'image
docker image history bdms-auth:1.0.0  # Chaque ligne = une couche (layer)

# Lancer le conteneur depuis l'image construite (plus de -v pour le code)
docker run -d   --name auth-service   -p 3001:3001   -e MONGO_URI=mongodb://host.docker.internal:27017   -e NOM_BASE=bdms-auth   -e JWT_SECRET=secret_dev_123   -e JWT_EXPIRES_IN=24h   bdms-auth:1.0.0   # Image construite — le code est DANS l'image
ℹ Modifier le code et observer l'invalidation du cache
  • Modifier auth-service/routes/auth.js → ajouter console.log('v1.0.1')
  • Reconstruire : docker build -t bdms-auth:1.0.1 .
  • Step 3 (COPY package*.json) → cache HIT (package.json non modifie)
  • Step 4 (RUN npm install) → cache HIT (dependances non modifiees)
  • Step 5 (COPY . .) → cache MISS (code source modifie)
  • npm install n'est PAS re-execute → gain de 45 secondes

3
.dockerignore et optimisation du cache
Contexte de build · node_modules · Ordre optimal
⚠ Probleme : npm install se re-execute a chaque modification de code

Si l'ordre Dockerfile est COPY . . → RUN npm install, chaque modification de index.js invalide la couche COPY . . → npm install re-execute (45 sec). Ordre optimal : COPY package*.json ./ → RUN npm install → COPY . . → npm install en cache si package.json inchange.

⚠ Probleme : docker build envoie 300 Mo au daemon

Sans .dockerignore, docker build envoie node_modules (~300 Mo) au daemon Docker. COPY . . le copie dans l'image, puis RUN npm install en cree un nouveau → doublon inutile. Avec .dockerignore : contexte = ~1 Mo, temps = < 1 seconde.

textauth-service/.dockerignore
node_modules    # 300 Mo — npm install les cree dans l'image
.env            # Ne jamais inclure les secrets dans l'image
.env.local
.env.*

*.log
npm-debug.log*

.git
.gitignore
README.md
Dockerfile
.dockerignore

4
TP : Dockerfile de produit-service
Ecriture autonome · Build · 10 tests Postman

En s'appuyant sur le Dockerfile de auth-service comme modele, ecrire seul :

  1. produit-service/.dockerignore
  2. produit-service/Dockerfile — image node:18-alpine, port 3002, NODE_ENV=production, commande node index.js
✓ Corrige — produit-service/Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
ENV NODE_ENV=production
ENV PORT=3002
EXPOSE 3002
CMD ["node", "index.js"]
bashBuild et lancement produit-service
cd app-microservices/produit-service
docker build -t bdms-produit:1.0.0 .
docker run -d --name produit-service -p 3002:3002   -e MONGO_URI=mongodb://host.docker.internal:27017   -e NOM_BASE=bdms-produits   -e AUTH_SERVICE_URL=http://host.docker.internal:3001   bdms-produit:1.0.0

10 tests de validation

  1. docker ps → auth-service et produit-service affichent Running
  2. docker logs produit-service → message de demarrage visible
  3. GET http://localhost:3002/health → 200
  4. GET http://localhost:3002/produits → 200 (liste — route publique)
  5. POST http://localhost:3001/auth/login → 200 — recuperer token
  6. POST http://localhost:3002/produits avec token → 201
  7. GET http://localhost:3002/produits/:id → 200
  8. DELETE http://localhost:3002/produits/:id sans token → 401
  9. docker exec -it produit-service sh → ls /app → code source dans l'image
  10. docker exec produit-service node --version → v18.x.x (version exacte de l'image)

✓
Dockerfile a trous — commande-service (6 blancs)
dockerfilecommande-service/Dockerfile — completer les blancs
① ________  node:18-alpine
② ________  /app
③ ________  package*.json ./
RUN npm install ④ ________
⑤ ________  .  .
ENV NODE_ENV=production
ENV PORT=3003
EXPOSE 3003
⑥ ________  ["node", "index.js"]
✓ Corrige

① FROM   ② WORKDIR   ③ COPY   ④ --production   ⑤ COPY   ⑥ CMD

Pourquoi COPY . . est place APRES RUN npm install dans un Dockerfile bien optimise ?
Partie 4 — Docker docker-compose.yml · up · down
⏱ 16 min

Docker Compose — Orchestrer plusieurs conteneurs

Equivalences docker run → YAML, structure complete docker-compose.yml (services/build/ports/environment/depends_on/networks/volumes), DNS interne par nom de service, 8 commandes Compose, TP architecture 4 services, service notif-service a trous.

✓ Ce que vous allez apprendre
  • Traduction de docker run en YAML docker-compose.yml
  • Structure complete : services, image/build, ports, environment, depends_on, networks, volumes
  • 8 commandes Docker Compose essentielles
  • Communication inter-services par nom de service (DNS interne)
  • TP : docker-compose.yml complet auth + produit + commande + MongoDB

1
De docker run repete au fichier declaratif
Equivalences YAML · Probleme des variables sensibles · Orchestration
Option docker runCle docker-compose.ymlExemple
--name auth-serviceNom du service (cle sous services:)auth-service:
-p 3001:3001ports:- "3001:3001"
-e PORT=3001environment:PORT: 3001
bdms-auth:1.0.0 (image)image:image: bdms-auth:1.0.0
Pas d'equivalent directbuild:context: ./auth-service
-v mongo_data:/data/dbvolumes:- mongo_data:/data/db
--network bdms-networknetworks:- bdms-network
Lancer apres mongodbdepends_on:- mongodb

2
docker-compose.yml complet — auth + produit + commande + MongoDB
build · depends_on · volumes nommes · DNS interne
yamlapp-microservices/docker-compose.yml
version: '3.9'

services:

  # ── MongoDB ──────────────────────────────────────────────────
  mongodb:
    image: mongo:7.0
    container_name: bdms-mongodb
    ports:
      - "27017:27017"         # Acces MongoDB Compass depuis l'hote
    environment:
      MONGO_INITDB_ROOT_USERNAME: admin
      MONGO_INITDB_ROOT_PASSWORD: secret
    volumes:
      - mongo_data:/data/db   # Volume nomme — donnees persistantes
    networks:
      - bdms-network

  # ── auth-service ─────────────────────────────────────────────
  auth-service:
    build:
      context: ./auth-service  # Construire l'image depuis le Dockerfile
    container_name: bdms-auth
    ports:
      - "3001:3001"
    environment:
      PORT: 3001
      MONGO_URI: mongodb://admin:secret@mongodb:27017  # 'mongodb' = nom du service
      NOM_BASE: bdms-auth
      JWT_SECRET: secret_dev_123
      JWT_EXPIRES_IN: 24h
    depends_on:
      - mongodb
    networks:
      - bdms-network
    restart: on-failure   # Redemarrer si MongoDB pas encore pret

  # ── produit-service ──────────────────────────────────────────
  produit-service:
    build:
      context: ./produit-service
    container_name: bdms-produit
    ports:
      - "3002:3002"
    environment:
      PORT: 3002
      MONGO_URI: mongodb://admin:secret@mongodb:27017
      NOM_BASE: bdms-produits
      AUTH_SERVICE_URL: http://auth-service:3001  # 'auth-service' = nom du service
    depends_on:
      - mongodb
      - auth-service
    networks:
      - bdms-network
    restart: on-failure

  # ── commande-service ─────────────────────────────────────────
  commande-service:
    build:
      context: ./commande-service
    container_name: bdms-commande
    ports:
      - "3003:3003"
    environment:
      PORT: 3003
      MONGO_URI: mongodb://admin:secret@mongodb:27017
      NOM_BASE: bdms-commandes
      AUTH_SERVICE_URL: http://auth-service:3001
      PRODUIT_SERVICE_URL: http://produit-service:3002
    depends_on:
      - mongodb
      - auth-service
      - produit-service
    networks:
      - bdms-network
    restart: on-failure

volumes:
  mongo_data:     # Docker gere le stockage — persiste entre down/up

networks:
  bdms-network:
    driver: bridge  # Reseau isole — les services se contactent par leur nom
ℹ DNS interne Docker Compose — communication par nom de service

Dans la Partie 3, produit-service appelait http://localhost:3001/auth/verify. Dans Docker Compose, localhost depuis un conteneur = le conteneur lui-meme, pas l'hote. La variable AUTH_SERVICE_URL: http://auth-service:3001 utilise auth-service comme hostname. Docker Compose cree un DNS interne : chaque service est resolvable par son nom de service. Communication interne → noms de services. Ports exposes → uniquement pour Postman/Nginx depuis l'hote.


3
8 commandes Docker Compose
up · down · ps · logs · exec · build · restart · scale
bash8 manipulations guidees
cd app-microservices  # Se placer la ou est docker-compose.yml

# 1 — Demarrer
docker-compose up -d          # detached — libere le terminal
docker-compose up --build -d  # Force le rebuild avant demarrage

# 2 — Lister les services
docker-compose ps             # Etat de tous les services

# 3 — Logs
docker-compose logs           # Tous les services
docker-compose logs auth-service
docker-compose logs -f auth-service  # Temps reel

# 4 — Executer dans un service
docker-compose exec auth-service sh      # Shell interactif
docker-compose exec mongodb mongosh      # Shell MongoDB

# 5 — Arreter sans supprimer
docker-compose stop
docker-compose start          # Relancer les conteneurs arretes

# 6 — Arreter ET supprimer
docker-compose down           # Supprime conteneurs + reseau
docker-compose down -v        # Supprime aussi les volumes (donnees perdues)
docker-compose down --rmi all # Supprime aussi les images buildees

# 7 — Reconstruire une image
docker-compose build auth-service
docker-compose build --no-cache   # Rebuild sans cache

# 8 — Scaler
docker-compose up -d --scale auth-service=3  # 3 instances
docker-compose ps  # auth-service_1, _2, _3

✓
Service a trous — notif-service dans docker-compose.yml

Completer les 5 blancs. Bonus : expliquer pourquoi notif-service n'a pas de cle ports:.

yamlnotif-service dans docker-compose.yml — completer
  notif-service:
    ① ________ :
      context: ./notif-service
    container_name: bdms-notif
    ② ________ :
      RABBITMQ_URL: amqp://guest:guest@rabbitmq:5672
      EXCHANGE_NOM: commandes
    ③ ________ :
      - rabbitmq
    ④ ________ :
      - bdms-network
    restart: ⑤ ________
✓ Corrige
  • ① build — construire depuis Dockerfile
  • ② environment — variables pour RabbitMQ
  • ③ depends_on — demarrer rabbitmq avant notif-service
  • ④ networks — rejoindre bdms-network
  • ⑤ on-failure — RabbitMQ peut ne pas etre pret immediatement
  • Pas de ports: — notif-service est un consumer RabbitMQ pur, pas un serveur HTTP accessible depuis l'exterieur
depends_on: - mongodb garantit-il que MongoDB est pret a accepter des connexions quand auth-service demarre ?
Partie 4 — Docker Reseaux · Volumes · .env · healthcheck
⏱ 17 min

Reseaux Docker, volumes et Compose avance

3 types de reseaux (bridge, host, overlay), DNS interne Docker, 3 types de volumes (nomme, bind mount, tmpfs), fichier .env pour les secrets, reseaux multi-couches frontend/backend, healthcheck + condition:service_healthy, nginx.conf pour Compose.

✓ Ce que vous allez apprendre
  • 3 types de reseaux Docker : bridge, host, overlay
  • DNS interne Docker — comment les conteneurs se trouvent par nom
  • 3 types de volumes : volume nomme, bind mount, tmpfs
  • Fichier .env pour separer les secrets du docker-compose.yml
  • Reseaux multi-couches (frontend/backend) et healthcheck

1
3 types de reseaux Docker
Bridge · Host · Overlay · DNS interne
TypeIsolationCas d'usageCommande
BridgeReseau prive isole — DNS interne entre conteneurs du meme reseauDeveloppement local, Docker Compose (chaque projet a son reseau)docker network create --driver bridge
HostPartage le reseau de la machine hote — pas d'isolationPerformance maximale, Linux uniquement (pas Docker Desktop)docker run --network=host
OverlayReseau distribue multi-hotesProduction a grande echelle, Docker Swarm, KubernetesNon couvert dans ce module
ℹ DNS interne Docker — comment fonctionne la resolution de noms

Chaque reseau Docker bridge a un resolveur DNS integre (127.0.0.11 dans les conteneurs). Quand auth-service envoie une requete a mongodb:27017 :

  1. Le DNS interne Docker recoit la demande de resolution de 'mongodb'
  2. Il cherche un conteneur nomme 'mongodb' dans le meme reseau
  3. Il retourne l'IP interne du conteneur MongoDB (ex : 172.18.0.2)
  4. La connexion TCP s'etablit vers cette IP

Isolation par projet : Docker Compose prefixe les reseaux avec le nom du dossier. Projet A → app-a_bdms-network, Projet B → app-b_bdms-network — completement isoles.

bashExplorer les reseaux Docker
docker network ls                          # Lister tous les reseaux
docker network inspect app-microservices_bdms-network  # Conteneurs connectes + IPs

# Tester l'isolation — 2 conteneurs dans des reseaux differents
docker network create reseau-a
docker network create reseau-b
docker run -d --name cont-a --network reseau-a node:18-alpine sleep 300
docker run -d --name cont-b --network reseau-b node:18-alpine sleep 300
docker exec cont-a ping cont-b   # Attendu : ECHEC — reseaux isoles
docker stop cont-a cont-b && docker rm cont-a cont-b
docker network rm reseau-a reseau-b

2
3 types de volumes Docker
Volume nomme · Bind mount · tmpfs
TypeSyntaxeStockageCas d'usage
Volume nomme- mongo_data:/data/dbGere par Docker (/var/lib/docker/volumes/)Donnees persistantes : MongoDB, uploads
Bind mount- ./logs:/app/logsDossier exact de l'hote (chemin absolu ou relatif)Dev (hot reload), logs, configs (:ro)
tmpfstmpfs: /tmpMemoire RAM — jamais ecrit sur disqueDonnees temporaires sensibles, sessions
bashPratiquer les volumes
# Volume nomme — persiste apres suppression du conteneur
docker volume create test-vol
docker run -d --name vol-test -v test-vol:/data node:18-alpine sleep 300
docker exec vol-test sh -c "echo hello > /data/test.txt"
docker stop vol-test && docker rm vol-test
docker run --rm -v test-vol:/data node:18-alpine cat /data/test.txt  # hello persiste !
docker volume rm test-vol

# Bind mount — dossier hote visible dans le conteneur
mkdir -p /tmp/test-logs
docker run -d --name bind-test -v /tmp/test-logs:/app/logs node:18-alpine sleep 300
docker exec bind-test sh -c "echo log-entry > /app/logs/app.log"
cat /tmp/test-logs/app.log   # Visible sur l'hote immediatement
docker stop bind-test && docker rm bind-test

3
Compose avance : .env, reseaux multi-couches, healthcheck
Secrets · Isolation frontend/backend · service_healthy

Fichier .env — separer les secrets

bashapp-microservices/.env — NE PAS versionner dans Git
MONGO_ROOT_USER=admin
MONGO_ROOT_PASSWORD=motdepasse_fort_2024
JWT_SECRET=cle_secrete_longue_et_aleatoire_xyz789
JWT_EXPIRES_IN=24h
RABBITMQ_USER=rmquser
RABBITMQ_PASSWORD=rmqpassword
AUTH_PORT=3001
PRODUIT_PORT=3002
COMMANDE_PORT=3003

Docker Compose charge automatiquement le .env du meme dossier. Reference dans le YAML : ${VARIABLE}. Valeur par defaut : ${VARIABLE:-valeur_defaut}.

Reseaux multi-couches — isolation frontend/backend

  • bdms-frontend : Nginx + auth + produit + commande. Nginx peut contacter les services Node.js.
  • bdms-backend : auth + produit + commande + notif + MongoDB + RabbitMQ. Les services accedeent aux bases. Nginx N'EST PAS dans ce reseau — il ne peut pas contacter MongoDB directement.
ℹ healthcheck + condition: service_healthy
mongodb:
  image: mongo:7.0
  healthcheck:
    test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
    interval: 5s
    timeout: 10s
    retries: 5
    start_period: 10s  # Delai avant la 1ere verification

auth-service:
  depends_on:
    mongodb:
      condition: service_healthy  # Attend que MongoDB soit VRAIMENT pret
✓ nginx.conf pour Docker Compose — noms de services

Dans nginx.conf version Compose, les upstreams utilisent les noms de services Compose (pas localhost) :

upstream auth_service    { server auth-service:3001; }     # Nom de service Compose
upstream produit_service { server produit-service:3002; }
upstream commande_service{ server commande-service:3003; }

✓
Evaluation — 5 questions
Nginx est dans bdms-backend. Quelle est la faille de securite resultante ?
Quelle difference entre docker-compose stop et docker-compose down ?
Partie 4 — Docker TP consolidation · Resilience · Analytics
⏱ 18 min

TP Consolidation Partie 4 — Architecture complete

Audit de 4 erreurs docker-compose.yml (localhost, depends_on circulaire, :ro manquant, reseau fuite), checklist 10 points de demarrage, 6 scenarios de resilience (pannes, scaling, persistance), creation analytics-service consumer RabbitMQ.

✓ Ce que vous allez faire
  • Phase 1 : Identifier et corriger 4 erreurs dans un docker-compose.yml defectueux
  • Phase 2 : Demarrer l'architecture complete — checklist 10 points
  • Phase 3 : 6 scenarios de resilience (pannes, scaling, redemarrage)
  • Phase 4 : Creer analytics-service (consumer RabbitMQ + MongoDB)
  • Phase 5 : Rediger README.md de deploiement

1
Phase 1 — Audit : 4 erreurs dans le docker-compose.yml
Lire, identifier, corriger avant de voir le corrige
yamldocker-compose.yml avec 4 erreurs intentionnelles — identifier avant de lire le corrige
services:
  mongodb:
    image: mongo:7.0
    networks:
      - bdms-backend

  auth-service:
    build: { context: ./auth-service }
    ports:
      - "3001:3001"
    environment:
      MONGO_URI: mongodb://admin:secret@localhost:27017  # ERREUR 1
    depends_on:
      - auth-service  # ERREUR 2
    networks:
      - bdms-frontend
      - bdms-backend

  nginx:
    image: nginx:1.25-alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf  # ERREUR 3 — manque :ro
    networks:
      - bdms-frontend
      - bdms-backend  # ERREUR 4

networks:
  bdms-frontend:
  bdms-backend:
✓ Corrige — les 4 erreurs
  • Erreur 1 — localhost dans MONGO_URI : depuis un conteneur, localhost = le conteneur lui-meme. Correction : mongodb://admin:secret@mongodb:27017 (nom du service).
  • Erreur 2 — depends_on: - auth-service : un service ne peut pas dependre de lui-meme — boucle infinie. Correction : depends_on: - mongodb.
  • Erreur 3 — volume nginx.conf sans :ro : le conteneur nginx pourrait modifier le fichier de config sur l'hote. Correction : ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro.
  • Erreur 4 — nginx dans bdms-backend : faille de securite — nginx pourrait contacter MongoDB directement. Correction : supprimer - bdms-backend de nginx.
bashValider la syntaxe avant de demarrer
docker-compose config  # Valide le YAML et affiche la config resolue (.env inclus)

2
Phase 2 — Demarrage : checklist 10 points
Valider avant de passer aux scenarios de resilience
bashDemarrage et verification
cd app-microservices
docker-compose up --build -d   # Build + demarrage de tous les services
docker-compose ps              # Verifier que tous sont Running

Cocher chaque point. Ne pas passer a la Phase 3 tant que tous ne sont pas valides :

  1. docker-compose ps → 7 services affichent 'Up'
  2. docker-compose logs mongodb | grep 'Waiting for connections'
  3. docker-compose logs rabbitmq | grep 'Server startup complete'
  4. GET http://localhost/health → 200 { nginx: ok }
  5. GET http://localhost/auth/health → 200 (via Nginx)
  6. GET http://localhost/produits → 200 (liste)
  7. docker network ls | grep bdms → bdms-frontend ET bdms-backend presents
  8. docker volume ls | grep bdms → mongo_data et rabbitmq_data presents
  9. docker-compose exec nginx ping auth-service → PING reussi (meme reseau frontend)
  10. docker-compose exec nginx ping mongodb → ECHEC (reseaux differents — isolation validee)

3
Phase 3 — 6 scenarios de resilience
Noter le comportement observe AVANT de lire le resultat attendu
ScenarioActionResultat attendu
Sc 1 : Arreter notif-servicePOST /commandes → noter le code HTTP201 (commande creee). queue-notifs : 1 message en attente. Au redemarrage notif-service : consomme immediatement.
Sc 2 : Arreter produit-servicePOST /commandes et GET /produits503 pour les deux (via Nginx : error_page 502 503)
Sc 3 : Arreter auth-servicePOST /commandes et GET /produitsPOST /commandes : 503. GET /produits : 200 (route publique — ne contacte pas auth-service)
Sc 4 : Modifier .env (AUTH_PORT=3011)docker-compose up -d auth-serviceauth-service ecoute sur 3011. Nginx contacte toujours auth-service:3001 → 502
Sc 5 : Scaler commande-service × 3docker-compose up -d --scale commande-service=3Erreur 'port already in use' — les 3 instances essaient de binder le port 3003. Solution : supprimer ports: de commande-service
Sc 6 : down puis updocker-compose down puis up -dGET /produits : donnees presentes (volumes mongo_data non supprimes par down). down -v les supprimerait.

4
Phase 4 — analytics-service (consumer RabbitMQ + MongoDB)
Corrige fourni — implementation autonome d'abord

Creer app-microservices/analytics-service/ : consumer RabbitMQ qui ecoute l'exchange 'commandes' (fanout). A chaque evenement 'commande.creee', insere une entree dans MongoDB (bdms-analytics). Contraintes : port optionnel, reseau bdms-backend uniquement, Dockerfile node:18-alpine.

javascriptanalytics-service/index.js — corrige
const amqplib = require('amqplib');
const { MongoClient } = require('mongodb');

const RABBITMQ_URL = process.env.RABBITMQ_URL || 'amqp://localhost';
const MONGO_URI    = process.env.MONGO_URI    || 'mongodb://localhost:27017';
const NOM_BASE     = process.env.NOM_BASE     || 'bdms-analytics';
const EXCHANGE_NOM = 'commandes';

let db;

async function connecterMongo() {
  const client = new MongoClient(MONGO_URI);
  await client.connect();
  db = client.db(NOM_BASE);
  console.log('[ANALYTICS] MongoDB connecte');
}

async function demarrerConsumer() {
  const connexion = await amqplib.connect(RABBITMQ_URL);
  const channel   = await connexion.createChannel();
  await channel.assertExchange(EXCHANGE_NOM, 'fanout', { durable: true });
  const { queue } = await channel.assertQueue('queue-analytics', { durable: true });
  await channel.bindQueue(queue, EXCHANGE_NOM, '');

  channel.consume(queue, async (msg) => {
    if (!msg) return;
    const evt = JSON.parse(msg.content.toString());
    await db.collection('evenements').insertOne({
      ...evt, timestamp: new Date(), source: 'commande.creee'
    });
    console.log('[ANALYTICS] Enregistre :', evt.commandeId);
    channel.ack(msg);
  }, { noAck: false });

  console.log('[ANALYTICS] Consumer actif — ecoute queue-analytics');
}

connecterMongo().then(demarrerConsumer).catch(console.error);
yamlanalytics-service dans docker-compose.yml — corrige
  analytics-service:
    build: { context: ./analytics-service }
    container_name: bdms-analytics
    environment:
      MONGO_URI: mongodb://${MONGO_ROOT_USER}:${MONGO_ROOT_PASSWORD}@mongodb:27017
      NOM_BASE: bdms-analytics
      RABBITMQ_URL: amqp://${RABBITMQ_USER}:${RABBITMQ_PASSWORD}@rabbitmq:5672
      EXCHANGE_NOM: commandes
    depends_on:
      mongodb:
        condition: service_healthy
      rabbitmq:
        condition: service_healthy
    networks:
      - bdms-backend   # Acces MongoDB + RabbitMQ — pas d'acces HTTP entrant depuis Nginx
    restart: on-failure
docker-compose up --scale commande-service=3 echoue avec 'port already in use'. Quelle est la correction dans docker-compose.yml ?
Partie 5 — Azure Azure · ARM · CLI · Regions
⏱ 15 min

Introduction a Azure — Positionnement et gestion

AWS vs Azure vs GCP (parts de marche), services Azure de la Partie 5 (ACI/ACR/App Service), infrastructure mondiale (Regions/Zones/Paires), hierarchie de gestion (Tenant/Abonnement/Groupe/Ressource), ARM, Azure CLI, creation compte Free + premier groupe.

✓ Ce que vous allez apprendre
  • Positionnement Azure face a AWS et GCP — parts de marche et points forts
  • Infrastructure mondiale : Regions, Zones de disponibilite, Paires de regions
  • Hierarchie de gestion : Tenant, Abonnement, Groupe de ressources, Ressource
  • Azure Resource Manager (ARM) — la couche de controle unifiee
  • Creer un compte Azure Free + premier groupe de ressources avec Azure CLI

1
Azure et les 3 grands clouds
AWS · Azure · GCP · Services Partie 5
CloudFournisseurPart 2024Points forts
AWSAmazon~32%Leader — plus grand catalogue de services, pionnier
AzureMicrosoft~22%Integration Microsoft (Office 365, Active Directory, Windows Server)
GCPGoogle~11%Big Data, Machine Learning, Kubernetes (GKE — cree par Google)
Service AzureRoleEquivalent cloud-agnostique
Azure Container Instances (ACI)Deployer un conteneur Docker en quelques secondes — facturation a la secondeAWS Fargate
Azure Container Registry (ACR)Registry prive pour les images DockerDocker Hub (prive), ECR
Azure App ServiceHeberger une app Node.js — scaling automatiqueAWS Elastic Beanstalk, Heroku
Azure Cosmos DBNoSQL managee — API MongoDB compatibleMongoDB Atlas, AWS DocumentDB
Azure Service BusBroker de messages managee — similaire a RabbitMQAWS SQS/SNS, RabbitMQ Cloud
Azure PipelinesCI/CD — construire, tester, deployer automatiquementGitHub Actions, GitLab CI

2
Infrastructure mondiale : Regions, Zones, Paires
Latence · Haute disponibilite · Disaster Recovery
ℹ Region Azure

Ensemble de datacenters situes dans une zone geographique proche, relies par un reseau a tres faible latence. Azure compte plus de 60 regions mondiaux. Pour le Maroc : France Centre (Paris) offre la latence la plus faible (~50ms depuis Casablanca). UAE Nord (Dubai) est une alternative (~80ms) si la conformite regionale l'exige.

ℹ Zone de disponibilite (Availability Zone)

Au sein d'une region : 2 a 3 datacenters physiquement separes (alimentation, refroidissement, reseau independants). Si AZ-1 tombe → les ressources sur AZ-2 continuent → zero downtime. SLA avec zones : 99,99% (< 1h d'arret par an). SLA sans zone : 99,9% (< 9h par an). Analogie : agence bancaire avec 3 caisses independantes — si une tombe, les clients basculent sur les autres.

ℹ Paire de regions (Region Pair)

Chaque region est associee a une autre dans la meme geographie. Exemples : France Centre ↔ France Sud, Europe Ouest ↔ Europe Nord. Utilite : replication geographique des donnees (Azure Storage GRS), mises a jour Azure sequentielles, Disaster Recovery si une region entiere est detruite.


3
Hierarchie de gestion Azure — 4 niveaux
Tenant · Abonnement · Groupe de ressources · Ressource
NiveauNomRoleExemple module
1 (haut)Tenant (Azure AD)L'organisation — contient utilisateurs et groupesmonentreprise.onmicrosoft.com
2AbonnementUnite de facturation — une facture mensuelleAzure Free — 200$ de credit
3Groupe de ressourcesConteneur logique — supprimer le groupe supprime toutrg-bdms-dev (France Centre)
4 (bas)RessourceElement Azure individuel — dans un et un seul groupeACI auth-service, ACR bdms
ℹ Bonnes pratiques groupes de ressources
  • Un groupe par environnement : rg-bdms-dev (supprimer facilement), rg-bdms-prod (protege).
  • Facturation par groupe : voir combien coute chaque application dans Cost Management.
  • Suppression en bloc : az group delete --name rg-bdms-dev supprime TOUTES les ressources du groupe.

4
Azure CLI + portail + premier groupe de ressources
az login · az group create · Facturation · Compte Free
ℹ Azure Resource Manager (ARM)

ARM est la couche de controle centrale de toutes les ressources Azure. Toute interaction (portail, CLI, SDK, API) passe par ARM. ARM fournit : authentification (Azure AD), deploiement declaratif (ARM Templates/Bicep), controle d'acces (RBAC), tags et verrous. Analogie : ARM est le bureau d'accueil d'Azure — peu importe comment on entre, on passe toujours par ARM.

bashInstallation Azure CLI + premier groupe de ressources
# Installation Azure CLI
# Windows : https://aka.ms/installazurecliwindows
# macOS   : brew install azure-cli
# Linux   : curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash

az --version   # Verifier l'installation

# Connexion
az login       # Ouvre le navigateur — se connecter avec le compte Azure Free

# Abonnement actif
az account show
az account list --output table

# Creer le groupe de ressources
az group create --name rg-bdms-dev --location francecentral
# Attendu : { "provisioningState": "Succeeded" }

az group list --output table   # Verifier

# Supprimer en fin de seance (eviter la facturation)
az group delete --name rg-bdms-dev --yes --no-wait
⚠ Facturation Azure — regles pour ce module
  • Azure Container Instances : ~0.05$ pour 1 conteneur pendant 1h — acceptable.
  • Supprimer les ressources apres chaque seance : az group delete --name rg-bdms-dev --yes.
  • Surveiller les couts dans Cost Management + Billing dans le portail.
  • Ne jamais activer Pay-As-You-Go sans comprendre les couts.
  • Calculateur : azure.microsoft.com/fr-fr/pricing/calculator/

✓
Evaluation — 5 questions Azure fondamentaux
L'equipe execute az group delete --name rg-bdms-dev --yes. Quelles ressources sont supprimees ?
Quelle est la difference entre Azure Resource Manager (ARM) et le portail Azure ?
Partie 5 — Azure App Service · AKS · ACI
⏱ 17 min

Panorama des services Azure, App Service et AKS

Catalogue Azure par categorie (Compute/BDD/Conteneurs/DevOps), App Service plans (F1/B1/S1), Always On et cold start, mode code vs mode conteneur, comparaison Docker Compose / ACI / AKS, TP CLI creation App Service F1 + metriques + logs.

✓ Ce que vous allez apprendre
  • Panorama des services Azure par categorie : Compute, BDD, Conteneurs, DevOps
  • Azure App Service : PaaS, plans (F1/B1/S1), mode code vs mode conteneur
  • Always On — pourquoi le cold start detruit l'experience en production
  • Azure Kubernetes Service (AKS) vs ACI vs Docker Compose — quand choisir quoi
  • TP : creer un App Service F1 via CLI, explorer les metriques et les logs

1
Panorama des services Azure par categorie
Compute · Bases de donnees · Conteneurs · DevOps
ServiceTypeUsageUtilise en Partie 5
Azure Virtual MachinesIaaSVM Linux/Windows — controle total de l'OSNon (trop bas niveau)
Azure App ServicePaaSHeberger des apps Node.js, Python, .NET sans gerer de serveurSeances 31-34
Azure Container InstancesCaaSDeployer un conteneur Docker — facturation a la secondePanorama seance 32
Azure Kubernetes ServiceCaaSOrchestrer des centaines de conteneurs — K8s managePanorama seance 31
Azure Cosmos DBNoSQLAPI MongoDB compatible — remplace MongoDB localAlternative presentee
Azure Container RegistryRegistryRegistry prive pour images Docker — alternative Docker HubSeances 32-34
Azure PipelinesCI/CDBuild, test, deploiement automatise sur git pushSeances 35-36
Azure MonitorObservabiliteMetriques, logs, alertes de toutes les ressourcesSeance 34

2
Azure App Service — plans, modes de deploiement, Always On
F1 / B1 / S1 · Mode code vs mode conteneur · Cold start
ℹ Qu'est-ce qu'Azure App Service ?

App Service est un service PaaS — deployer une application web sans gerer de serveur ni de conteneur. Azure gere automatiquement l'OS, le runtime (Node.js, Python...), les mises a jour de securite, la scalabilite. Analogie : le taxi — la destination est fournie, pas besoin de conduire.

App Service Plan : l'infrastructure sous-jacente. Plusieurs App Services partagent le meme Plan. 1 Plan B1 peut heberger auth-service ET produit-service — facturation unique.

PlanRAMCPUPrix/moisLimites clesUsage
F1 (Free)1 GoPartageGratuit60 min CPU/jour, pas d'Always On, pas de domaine customTests et apprentissage uniquement
B1 (Basic)1.75 Go1 coeur~13 $/moisPas d'autoscaling, pas de slots de deploiementPremier deploiement production
S1 (Standard)1.75 Go1 coeur~70 $/mois5 slots, autoscaling jusqu'a 10 instancesProduction standard
P1v3 (Premium)1.75 Go2 coeurs~140 $/moisJusqu'a 30 instancesProduction haute charge
⚠ Always On — parametre critique pour la production

Plan F1 : Always On desactive. Si aucune requete pendant 20 minutes → l'application est mise en veille. Premiere requete suivante : 10-30 secondes de demarrage a froid (cold start) → timeout pour le client. Plan B1 et superieur : Always On activable — l'application reste toujours en memoire, aucun cold start. Pour ce module : plan F1 acceptable en TP. En production reelle : B1 minimum.

CritereMode codeMode conteneur
Dockerfile requisNon — Azure detecte le runtimeOui — image pre-construite
Deploiementgit push, ZIP deploy, Azure Pipelinesaz webapp config container set (tag de l'image)
ComportementDepend des runtimes supportes par AzureIdentique au local — meme image
Mise a jourRedeploi du codeChanger le tag de l'image — sans redeploi de code
Usage recommandeApp simple Node.js sans DockerArchitecture microservices conteneurisee

3
AKS — orchestration a grande echelle
Pod · Deployment · Self-healing · HPA · Rolling update
CritereDocker ComposeACIAKS
EnvironnementDeveloppement localDev / Staging cloudProduction a grande echelle
ScalingManuel (--scale) — conflit portManuel (nouvelles instances ACI)Automatique (HPA)
Self-healingrestart: on-failure seulementRedemarrage si crashRemplace pods + noeuds defaillants
Load balancingNginx manuelNon natifService K8s integre
CoutGratuit (local)Facturation a la secondeCout VMs + overhead (~70$/mois min)
ComplexiteFaibleMoyenneElevee

Concepts Kubernetes essentiels (panorama) :

  • Pod : unite de base — 1 ou plusieurs conteneurs qui tournent ensemble
  • Deployment : description declarative d'un pod et de son nombre de repliques
  • Service : point d'acces reseau stable — load balancing interne entre pods
  • HPA (Horizontal Pod Autoscaler) : ajoute des pods selon la charge CPU/memoire
  • Rolling update : deploiement progressif sans downtime — remplace 1 pod a la fois

AKS dans ce module : panorama uniquement. Les seances 31-34 utilisent App Service (plus simple). AKS est aborde en detail dans un module avance.


4
TP : App Service F1 via CLI — creer, configurer, explorer
az group · az appservice plan · az webapp · metriques · logs
bashEtape 1 — Creer le groupe + plan + App Service
# Groupe de ressources
az group create --name rg-bdms-dev --location francecentral

# App Service Plan Linux F1 (gratuit)
az appservice plan create   --name plan-bdms-dev   --resource-group rg-bdms-dev   --sku F1   --is-linux   # Obligatoire pour Node.js sur Linux et les conteneurs

# Web App (mode code Node.js 18)
az webapp create   --name bdms-auth-dev \           # Nom unique mondialement
  --resource-group rg-bdms-dev   --plan plan-bdms-dev   --runtime 'NODE:18-lts'

# Recuperer l'URL de l'application
az webapp show   --name bdms-auth-dev   --resource-group rg-bdms-dev   --query defaultHostName --output tsv
# → bdms-auth-dev.azurewebsites.net
bashEtape 2 — Configurer les variables d'environnement (App Settings)
az webapp config appsettings set   --name bdms-auth-dev   --resource-group rg-bdms-dev   --settings     PORT=8080     NODE_ENV=production     JWT_SECRET=secret_azure_dev_123     JWT_EXPIRES_IN=24h
# App Service utilise le port 8080 par defaut (pas 3001)

az webapp config appsettings list   --name bdms-auth-dev   --resource-group rg-bdms-dev   --output table
bashEtape 3 — Explorer metriques + logs dans le portail
# Surveillance > Metriques : CPU Percentage, Memory, Http 5xx, Response Time
# Surveillance > Flux de journaux : logs en temps reel (equivalent docker logs -f)
# Configuration : App Settings, connexion strings, Always On

# Streamer les logs depuis le terminal
az webapp log config   --name bdms-auth-dev   --resource-group rg-bdms-dev   --application-logging filesystem   --docker-container-logging filesystem

az webapp log tail   --name bdms-auth-dev   --resource-group rg-bdms-dev   # Ctrl+C pour arreter

# Nettoyage IMPORTANT - eviter la facturation
az group delete --name rg-bdms-dev --yes --no-wait
✓ Recap — commandes CLI App Service

az group create → az appservice plan create → az webapp create → az webapp config appsettings set → az webapp log tail → az webapp restart → az group delete


✓
Questions d'evaluation — 5 questions
App Service plan F1 — l'application ne recoit aucune requete pendant 25 minutes. Que se passe-t-il quand la prochaine requete arrive ?
Une equipe heberge auth-service et produit-service sur le meme App Service Plan B1. Quel est le principal risque ?
Partie 5 — Azure Docker Hub · docker tag · docker push
⏱ 17 min

Docker Hub — Publier et partager ses images

Registries Docker (Hub, ACR, GitHub CR), anatomie d'un nom d'image (registry/username/nom:tag), SemVer et pourquoi eviter :latest en production, docker tag (alias sans copie), login/push/pull, TP build+tag+push des 3 images + 15 tests.

✓ Ce que vous allez apprendre
  • Qu'est-ce qu'un registry Docker — Docker Hub, ACR, GitHub Container Registry
  • Anatomie complete d'un nom d'image : [registry/][username/]nom[:tag]
  • Versioning semantique (SemVer) applique aux images Docker — pourquoi eviter :latest en production
  • docker tag : creer un alias sans copier l'image (meme IMAGE ID)
  • Compte Docker Hub + docker login + docker push + verification pull
  • TP : publier bdms-auth, bdms-produit et bdms-commande — 15 tests de validation

1
Registries Docker — comparaison et architecture pull/push
Docker Hub · ACR · GitHub CR · Registry local
ℹ Qu'est-ce qu'un registry Docker ?

Un registry est un serveur de stockage et de distribution d'images Docker. Analogie : un registry Docker est a une image ce que GitHub est a du code source. Flux : Developpeur A : docker build → docker push → registry. Developpeur B : docker pull ← registry. Serveur Azure : docker pull ← registry → docker run.

RegistryTypeAccesCas d'usageCe module
Docker HubPublic + priveInternet — gratuit (public)Images officielles, projets open source, partage equipeSeance 32
Azure Container RegistryPriveAzure AD — securiseImages privees d'entreprise, integration ACI/AKSSeances 33-34
GitHub Container RegistryPublic + priveToken GitHubImages liees a un depot GitHub, CI/CD GitHub ActionsNon couvert
Registry localPrive self-hostedReseau localAir-gapped, sans acces InternetNon couvert

2
Anatomie d'un nom d'image et versioning semantique
registry/username/nom:tag · SemVer · Eviter :latest en production
textAnatomie complete d'un nom d'image Docker
Format : [registry/][username/]nom[:tag]

node:18-alpine
  └─ registry  : docker.io (implicite — Docker Hub)
  └─ username  : library (images officielles — implicite)
  └─ nom       : node
  └─ tag       : 18-alpine

moncompte/bdms-auth:1.0.0
  └─ registry  : docker.io (implicite)
  └─ username  : moncompte
  └─ nom       : bdms-auth
  └─ tag       : 1.0.0

monacr.azurecr.io/bdms-auth:1.0.0
  └─ registry  : monacr.azurecr.io (ACR — explicite)
  └─ nom       : bdms-auth
  └─ tag       : 1.0.0

Regles :
  Si registry omis  → Docker Hub par defaut
  Si tag omis       → :latest par defaut (a eviter en production)
⚠ Versioning semantique (SemVer) — pourquoi eviter :latest en production
  • MAJEUR.MINEUR.PATCH : 1.0.0 → changement incompatible = 2.0.0, nouvelle fonctionnalite = 1.1.0, correction = 1.0.1
  • :latest change a chaque docker push — pointe toujours vers la derniere image. Si une nouvelle version buguee est publiee, tous les deploiements utilisant :latest la recuperent automatiquement.
  • :1.0.0 est immuable — garantit le meme comportement a chaque deploiement.
  • Strategie : toujours taguer moncompte/bdms-auth:1.0.0 ET moncompte/bdms-auth:latest. Utiliser :1.0.0 en production, :latest uniquement en dev.
bashdocker tag — creer un alias sans copier l'image
docker images | grep bdms-auth   # Affiche : bdms-auth  1.0.0

# docker tag SOURCE DESTINATION
docker tag bdms-auth:1.0.0 VOTRE_USERNAME/bdms-auth:1.0.0
docker tag bdms-auth:1.0.0 VOTRE_USERNAME/bdms-auth:latest

docker images | grep bdms-auth
# 3 lignes affichees — MAIS meme IMAGE ID pour les 3
# docker tag cree un pointeur (alias), pas une copie
# Supprimer un alias ne supprime pas l'image si d'autres tags pointent vers elle

3
Compte Docker Hub + docker login + premier push
hub.docker.com · docker login · docker push · couches partagees
bashEtape 1 a 4 — login, tag, push, verification
# 1 — Se connecter
docker login
# Username: VOTRE_USERNAME
# Password: VOTRE_MOT_DE_PASSE
# Attendu : Login Succeeded
# Credentials stockes dans ~/.docker/config.json — ne jamais partager

# 2 — Construire si necessite
cd app-microservices/auth-service
docker build -t bdms-auth:1.0.0 .

# 3 — Taguer avec le prefixe username/
docker tag bdms-auth:1.0.0 VOTRE_USERNAME/bdms-auth:1.0.0
docker tag bdms-auth:1.0.0 VOTRE_USERNAME/bdms-auth:latest

# 4 — Pousser vers Docker Hub
docker push VOTRE_USERNAME/bdms-auth:1.0.0
# Observer la sortie : chaque couche — Preparing > Waiting > Pushing > Pushed
# Si :latest pointe vers la meme image → Layer already exists (push instantane)
docker push VOTRE_USERNAME/bdms-auth:latest

# 5 — Verifier le pull depuis une machine vierge (simulation)
docker rmi VOTRE_USERNAME/bdms-auth:1.0.0 VOTRE_USERNAME/bdms-auth:latest bdms-auth:1.0.0
docker images | grep bdms-auth   # Doit etre vide
docker pull VOTRE_USERNAME/bdms-auth:1.0.0   # Telecharge depuis Docker Hub
docker run -d --name auth-test -p 3001:3001   -e MONGO_URI=mongodb://host.docker.internal:27017   -e JWT_SECRET=test VOTRE_USERNAME/bdms-auth:1.0.0
docker logs auth-test   # Attendu : auth-service : http://localhost:3001
docker stop auth-test && docker rm auth-test

4
TP : publier bdms-produit et bdms-commande — 15 tests
bashPublier bdms-produit et bdms-commande
# bdms-produit
cd app-microservices/produit-service
docker build -t bdms-produit:1.0.0 .
docker tag bdms-produit:1.0.0 VOTRE_USERNAME/bdms-produit:1.0.0
docker tag bdms-produit:1.0.0 VOTRE_USERNAME/bdms-produit:latest
docker push VOTRE_USERNAME/bdms-produit:1.0.0
docker push VOTRE_USERNAME/bdms-produit:latest

# bdms-commande
cd ../commande-service
docker build -t bdms-commande:1.0.0 .
docker tag bdms-commande:1.0.0 VOTRE_USERNAME/bdms-commande:1.0.0
docker tag bdms-commande:1.0.0 VOTRE_USERNAME/bdms-commande:latest
docker push VOTRE_USERNAME/bdms-commande:1.0.0
docker push VOTRE_USERNAME/bdms-commande:latest

15 tests de validation

Docker Hub (portail web) : T1 : hub.docker.com/u/VOTRE_USERNAME → 3 depots visibles. T2 : bdms-auth → tags 1.0.0 et latest presents. T3 : bdms-produit → 1.0.0 et latest. T4 : bdms-commande → 1.0.0 et latest. T5 : cliquer tag 1.0.0 de bdms-auth → voir les couches.

Nommage et tags locaux : T6 : docker images | grep VOTRE_USERNAME → 6 lignes (3 images × 2 tags). T7 : docker inspect VOTRE_USERNAME/bdms-auth:1.0.0 → noter l'IMAGE ID. T8 : docker inspect VOTRE_USERNAME/bdms-auth:latest → meme IMAGE ID — confirme que tag latest est un alias.

Pull et execution : T9 : docker rmi VOTRE_USERNAME/bdms-produit:1.0.0. T10 : docker pull VOTRE_USERNAME/bdms-produit:1.0.0. T11 : docker images | grep bdms-produit → image recuperee.

Securite : T12 : docker logout. T13 : docker push VOTRE_USERNAME/bdms-auth:1.0.0 → unauthorized (non connecte). T14 : docker login → Login Succeeded. T15 : docker search VOTRE_USERNAME/bdms-auth → image visible.

✓ Flux complet a memoriser

1. docker build -t bdms-auth:1.0.0 . → 2. docker tag bdms-auth:1.0.0 username/bdms-auth:1.0.0 → 3. docker push username/bdms-auth:1.0.0 → 4. docker pull username/bdms-auth:1.0.0 (autre machine) → 5. docker run username/bdms-auth:1.0.0


✓
Evaluation — 5 questions
docker push retourne 'Layer already exists' pour toutes les couches. Que s'est-il passe ?
En production, bdms-auth:latest est deploye. Un developpeur publie accidentellement une version buguee sous le tag latest. Quel est l'impact ?
Partie 5 — Azure az webapp · WEBSITES_PORT · App Settings
⏱ 17 min

Azure App Service — Plans et deploiement depuis Docker Hub

Creer une Web App conteneur (portail + CLI), WEBSITES_PORT (variable critique), App Settings pour les secrets (jamais dans l'image), az webapp log tail, mise a jour de l'image sans recreer l'App Service, TP deploiement bdms-produit + 15 tests.

✓ Ce que vous allez apprendre
  • Creer une Web App en mode conteneur depuis Docker Hub — portail + CLI
  • WEBSITES_PORT — la variable critique sans laquelle App Service renvoie 503
  • App Settings : pourquoi ne jamais mettre les secrets dans l'image Docker
  • Mettre a jour l'image sans recrer l'App Service : az webapp config container set
  • Logs streaming : az webapp log tail — equivalent de docker logs -f
  • TP : deployer bdms-produit sur App Service — 15 tests de validation

1
Creer une Web App conteneur — portail + CLI
az webapp create · --deployment-container-image-name · URL HTTPS auto

Etapes via le portail Azure

  1. Ouvrir https://portal.azure.com → App Services → Creer → Application web
  2. Abonnement : Azure Free · Groupe : rg-bdms-dev · Nom : bdms-auth-dev
  3. Publier : Conteneur Docker ← choisir cette option (pas Code)
  4. Systeme d'exploitation : Linux · Region : France Centre
  5. Onglet Docker : Conteneur unique · Source : Docker Hub · Acces : Public · Image : VOTRE_USERNAME/bdms-auth:1.0.0
  6. Plan : F1 (gratuit) → Verifier + creer
  7. Attendre ~2 minutes → URL HTTPS automatique : https://bdms-auth-dev.azurewebsites.net
bashMeme resultat via la CLI — a maitriser pour l'automatisation
# Groupe de ressources
az group create --name rg-bdms-dev --location francecentral

# App Service Plan Linux F1
az appservice plan create   --name plan-bdms-dev   --resource-group rg-bdms-dev   --sku F1 --is-linux

# Web App en mode conteneur depuis Docker Hub
az webapp create   --name bdms-auth-dev   --resource-group rg-bdms-dev   --plan plan-bdms-dev   --deployment-container-image-name VOTRE_USERNAME/bdms-auth:1.0.0

# Recuperer l'URL
az webapp show   --name bdms-auth-dev   --resource-group rg-bdms-dev   --query defaultHostName --output tsv
# → bdms-auth-dev.azurewebsites.net
ℹ Ce qui se passe apres az webapp create

Azure telecharge VOTRE_USERNAME/bdms-auth:1.0.0 depuis Docker Hub. Il lance le conteneur sur le serveur du Plan. Il attribue automatiquement une URL HTTPS avec un certificat Let's Encrypt. Premier demarrage : 1-3 minutes (telechargement de l'image). Suivants : quelques secondes (image en cache). Verifier : App Service → Centre de deploiement → Journaux.


2
WEBSITES_PORT et App Settings — configuration correcte
WEBSITES_PORT · Secrets hors image · process.env
⚠ WEBSITES_PORT — variable critique

App Service envoie le trafic HTTP vers le port 80 du conteneur par defaut. auth-service ecoute sur 3001 → App Service ne sait pas ou router → 503 Service Unavailable. Solution : WEBSITES_PORT=3001 dans les App Settings. App Service route alors vers le port 3001 du conteneur. Regle : WEBSITES_PORT = port d'ecoute de l'application.

⚠ Pourquoi ne pas mettre les secrets dans l'image Docker

Une image Docker Hub publique est lisible par tous — MONGO_URI et JWT_SECRET visibles par n'importe qui. Meme avec une image privee : les secrets dans l'image sont dans chaque couche — risque. Solution Azure : App Settings — stockes dans Azure, chiffres au repos, injectes au runtime. Dans le conteneur : process.env.JWT_SECRET (identique a -e JWT_SECRET=abc en docker run).

bashConfigurer WEBSITES_PORT + variables d'environnement
az webapp config appsettings set   --name bdms-auth-dev   --resource-group rg-bdms-dev   --settings     WEBSITES_PORT=3001 \         # CRITIQUE — port ecoute de auth-service
    NODE_ENV=production     MONGO_URI='mongodb://admin:mdp@host:27017'     NOM_BASE=bdms-auth     JWT_SECRET=secret_azure_prod_xyz \   # Jamais dans l'image
    JWT_EXPIRES_IN=24h

az webapp config appsettings list   --name bdms-auth-dev   --resource-group rg-bdms-dev   --output table

3
Logs, redemarrage et mise a jour de l'image
az webapp log tail · az webapp restart · az webapp config container set
bashLogs, redemarrage et changement d'image
# Activer les logs
az webapp log config   --name bdms-auth-dev   --resource-group rg-bdms-dev   --application-logging filesystem \      # Logs Node.js (console.log)
  --docker-container-logging filesystem   # Logs du conteneur (demarrage, erreurs)

# Streamer les logs en temps reel (equivalent : docker logs -f)
az webapp log tail   --name bdms-auth-dev   --resource-group rg-bdms-dev   # Ctrl+C pour arreter

# Redemarrer (apres changement de config)
az webapp restart   --name bdms-auth-dev   --resource-group rg-bdms-dev

# Mettre a jour l'image sans recreer l'App Service
az webapp config container set   --name bdms-auth-dev   --resource-group rg-bdms-dev   --docker-custom-image-name VOTRE_USERNAME/bdms-auth:1.0.1  # Nouvelle version
# App Service telecharge la nouvelle image et redemarre automatiquement

4
TP : deployer bdms-produit — 15 tests de validation
bashDeployer bdms-produit sur le meme plan
# Creer l'App Service produit-service
az webapp create   --name bdms-produit-dev   --resource-group rg-bdms-dev   --plan plan-bdms-dev \           # Meme plan que auth-service
  --deployment-container-image-name VOTRE_USERNAME/bdms-produit:1.0.0

# Configurer les variables
az webapp config appsettings set   --name bdms-produit-dev   --resource-group rg-bdms-dev   --settings     WEBSITES_PORT=3002     NODE_ENV=production     MONGO_URI='mongodb://admin:mdp@host:27017'     NOM_BASE=bdms-produits     AUTH_SERVICE_URL=https://bdms-auth-dev.azurewebsites.net  # URL Azure de auth-service

# Activer les logs
az webapp log config   --name bdms-produit-dev   --resource-group rg-bdms-dev   --application-logging filesystem   --docker-container-logging filesystem
#TestAttendu
T1az webapp list --resource-group rg-bdms-dev --output tablebdms-auth-dev et bdms-produit-dev affichent Running
T2az webapp show --name bdms-auth-dev ... --query stateRunning
T3az webapp log tail --name bdms-auth-dev ...Logs visibles en temps reel
T4GET https://bdms-auth-dev.azurewebsites.net/health200 { service: 'auth-service' }
T5GET https://bdms-produit-dev.azurewebsites.net/health200 { service: 'produit-service' }
T6POST .../auth/register201 ou 500 (normal si MongoDB non connecte)
T7GET .../produits200 (route publique)
T8az webapp config appsettings list --name bdms-produit-devWEBSITES_PORT=3002 visible
T9Portail → bdms-produit-dev → Configuration → AUTH_SERVICE_URLURL Azure de auth-service
T10az webapp config container show --name bdms-auth-dev ...image: VOTRE_USERNAME/bdms-auth:1.0.0
T11az webapp config container set ... --docker-custom-image-name .../bdms-auth:latestApp Service redemarrage avec nouvelle image
T12az webapp restart --name bdms-auth-dev ...Redemarrage et recovery automatique
T13az webapp log tail apres restartRedemarrage visible dans les logs
T14Portail → Surveillance → Metriques → CPU + Http 2xxGraphiques visibles
T15az group delete --name rg-bdms-dev --yes --no-waitSuppression de toutes les ressources

✓
Tableau comparatif — 3 modes de deploiement App Service
CritereMode codeMode conteneurACI
Dockerfile requisNonOuiOui
Image Docker HubNonOui — pull autoOui — pull auto
URL HTTPS autoOuiOuiOui (IP publique)
Always OnB1+ uniquementB1+ uniquementToujours actif
FacturationMensuelle fixeMensuelle fixeA la seconde
Mise a jour imageN/Aaz webapp config container setaz container create (recreer)
Logsaz webapp log tailaz webapp log tailaz container logs
produit-service ecoute sur le port 3002. App Service retourne 503 malgre un conteneur qui tourne. Quelle est la cause la plus probable ?
Partie 5 — Azure Multi-conteneurs · Log Stream · Metriques
⏱ 18 min

Multi-conteneurs Azure, Surveillance et TP final Cloud Native

docker-compose.azure.yml (differences avec Compose local : pas de build, pas de bind mount), deploiement multi-conteneurs az webapp, DNS interne Azure (plus rapide que URL publique), 3 outils de surveillance (Log Stream, Diagnostics, Metriques), TP final 20 tests flux complet.

✓ Ce que vous allez faire
  • Differences entre docker-compose.azure.yml et le Compose local — ce qui change, ce qui reste
  • Deployer auth + produit + commande en multi-conteneurs sur un seul App Service
  • DNS interne Azure — communication inter-services par nom de service (plus rapide que l'URL publique)
  • 3 outils de surveillance : Log Stream, Application Diagnostics, Metriques Azure
  • TP final — architecture complete deployee sur Azure : flux register → login → produit → commande en 20 tests

1
Deploiement multi-conteneurs sur App Service
docker-compose.azure.yml · Differences avec le Compose local · DNS interne Azure
Fonctionnalitedocker-compose.yml local (Partie 4)docker-compose.azure.yml (Azure)
build:Construit l'image depuis un DockerfileInterdit — uniquement images pre-construites (image:)
Volumes bind mountMonter ./logs:/app/logsNon supporte — utiliser le stockage Azure
depends_on condition:service_healthy fonctionneNon supporte — simplifie
Port exposeTous les services peuvent exposer des portsUn seul service expose un port vers l'exterieur
DNS interneNom de service Compose = hostnameMeme principe — nom de service = hostname DNS Azure
SecretsVariables en clair dans le YAML ou .envVariables referencees avec ${VAR} — injectees via App Settings Azure
yamlapp-microservices/docker-compose.azure.yml
version: '3.9'

services:

  auth-service:
    image: VOTRE_USERNAME/bdms-auth:1.0.0   # Image pre-construite — pas de build:
    environment:
      WEBSITES_PORT: 3001
      PORT: 3001
      NODE_ENV: production
      MONGO_URI: ${MONGO_URI}       # Injecte depuis App Settings Azure
      NOM_BASE: bdms-auth
      JWT_SECRET: ${JWT_SECRET}     # Jamais en clair ici
      JWT_EXPIRES_IN: 24h
    ports:
      - '3001:3001'

  produit-service:
    image: VOTRE_USERNAME/bdms-produit:1.0.0
    environment:
      PORT: 3002
      NODE_ENV: production
      MONGO_URI: ${MONGO_URI}
      NOM_BASE: bdms-produits
      AUTH_SERVICE_URL: http://auth-service:3001  # DNS interne Azure = nom de service
    ports:
      - '3002:3002'

  commande-service:
    image: VOTRE_USERNAME/bdms-commande:1.0.0
    environment:
      PORT: 3003
      NODE_ENV: production
      MONGO_URI: ${MONGO_URI}
      NOM_BASE: bdms-commandes
      AUTH_SERVICE_URL: http://auth-service:3001
      PRODUIT_SERVICE_URL: http://produit-service:3002
    ports:
      - '3003:3003'
bashDeployer le docker-compose.azure.yml sur App Service
az group create --name rg-bdms-final --location francecentral

az appservice plan create   --name plan-bdms-final   --resource-group rg-bdms-final   --sku B1 --is-linux   # B1 recommande pour le multi-conteneurs

az webapp create   --name bdms-final-VOTRE_NOM \   # Nom unique mondialement
  --resource-group rg-bdms-final   --plan plan-bdms-final   --multicontainer-config-type compose \   # Mode multi-conteneurs
  --multicontainer-config-file docker-compose.azure.yml

# Injecter les secrets via App Settings (jamais dans le YAML)
az webapp config appsettings set   --name bdms-final-VOTRE_NOM   --resource-group rg-bdms-final   --settings     MONGO_URI='mongodb://admin:mdp@host:27017'     JWT_SECRET=secret_azure_final
ℹ DNS interne Azure multi-conteneurs — plus rapide et securise

Dans docker-compose.azure.yml, produit-service contacte auth-service via AUTH_SERVICE_URL: http://auth-service:3001. Azure cree un reseau DNS interne — le nom de service est resolu vers l'IP interne du conteneur. Comparaison avec la seance 33 :

  • Seance 33 (services separes) : AUTH_SERVICE_URL = https://bdms-auth-dev.azurewebsites.net → trafic sort sur Internet puis revient — latence inutile.
  • Seance 34 (multi-conteneurs) : AUTH_SERVICE_URL = http://auth-service:3001 → reseau prive Azure — plus rapide, plus securise, pas de cout reseau sortant.

2
Surveillance et logs — 3 outils Azure
Log Stream · Application Diagnostics · Metriques · Alertes
OutilAccesUsageEquivalent local
Log StreamPortail : Surveillance → Flux de journaux · CLI : az webapp log tailLogs en temps reel — deboguer un probleme en direct, observer le demarrage des conteneursdocker logs -f ou docker-compose logs -f
App DiagnosticsPortail : Diagnostiquer et resoudre les problemesAnalyse automatique : taux d'erreur, problemes de conteneur, erreurs HTTP 4xx/5xx avec trace completedocker inspect + lecture manuelle des logs
MetriquesPortail : Surveillance → MetriquesCPU, memoire, Http 2xx/5xx, temps de reponse, nombre de requetes — graphiques et alertesdocker stats

Metriques essentielles a surveiller

  • CPU Percentage : > 80% de facon prolongee → envisager scale up vers P1v3
  • Memory working set : montee progressive sans descente → fuite memoire Node.js (references retenues indefiniment)
  • Http 5xx : pic soudain → crash de l'application ou surcharge de la base de donnees
  • Response time : > 2 secondes → probleme de performance ou requete MongoDB lente
bashActiver et utiliser les logs
# Activer les logs
az webapp log config   --name bdms-final-VOTRE_NOM   --resource-group rg-bdms-final   --application-logging filesystem \     # console.log de Node.js
  --docker-container-logging filesystem  # demarrage conteneur, erreurs Docker

# Streamer en temps reel
az webapp log tail   --name bdms-final-VOTRE_NOM   --resource-group rg-bdms-final

# Telecharger les logs en archive
az webapp log download   --name bdms-final-VOTRE_NOM   --resource-group rg-bdms-final   --log-file logs.zip

3
TP final — architecture complete sur Azure, 20 tests
Flux complet register → login → produit → commande
ℹ Objectif du TP final

A la fin de ce TP, l'architecture microservices de la Partie 3 est entierement deployee sur Azure. C'est la demonstration finale de la competence Cloud Native : code ecrit en Partie 2 → conteneurise en Partie 4 → deploye sur Azure en Partie 5.

bashEtape 1 — Verifier les 3 images sur Docker Hub
docker images | grep VOTRE_USERNAME   # Verifier les images locales

# Si une image manque — reconstruire et pousser
cd app-microservices/commande-service
docker build -t bdms-commande:1.0.0 .
docker tag bdms-commande:1.0.0 VOTRE_USERNAME/bdms-commande:1.0.0
docker push VOTRE_USERNAME/bdms-commande:1.0.0
#TestAttendu
T1az webapp list --resource-group rg-bdms-final --output tablebdms-final-VOTRE_NOM affiche Running
T2az webapp log tailLogs des 3 services (auth, produit, commande) visibles
T3Portail → Diagnostiquer et resoudre → aucune alerte critiqueSante app OK
T4GET https://bdms-final-VOTRE_NOM.azurewebsites.net/health200 (service principal)
T5az webapp config appsettings list --name bdms-final-VOTRE_NOMMONGO_URI et JWT_SECRET presents
T6POST .../auth/register { email, motDePasse, nom }201 (si MongoDB connecte)
T7POST .../auth/login → recuperer token_azure200 + token JWT
T8GET .../auth/verify avec token_azure200 { valide: true }
T9GET .../produits200 (route publique)
T10POST .../produits avec token_azure { nom, prix, stock, categorie }201 + _id du produit
T11GET .../produits/:id200 + champ disponible calcule
T12POST .../commandes avec token_azure { produitId, quantite }201 commande creee
T13GET .../commandes avec token_azure200 liste des commandes
T14Portail → Metriques → CPU PercentageGraphique visible, pic apres les tests Postman
T15Portail → Metriques → Http 2xxPic correspond aux requetes Postman
T16az webapp log download ... --log-file logs.zipFichier logs.zip telecharge
T17az webapp config container set ... --multicontainer-config-file docker-compose.azure.ymlRedeploiement sans downtime
T18az webapp log tail pendant T17Redemarrage des 3 conteneurs visible
T19az webapp config appsettings set ... --settings NODE_ENV=stagingVariable mise a jour sans rebuild ni recreer l'App Service
T20az group delete --name rg-bdms-final --yes --no-waitSuppression de toutes les ressources — nettoyage final

✓
Architecture finale sur Azure — schema
textSchema d'architecture — reproduire sur le cahier
             INTERNET
                 |
    Azure App Service (bdms-final-VOTRE_NOM)
    Plan B1 — France Centre — HTTPS automatique
    ┌────────────────────────────────────────────┐
    │  auth-service:3001                          │
    │  produit-service:3002                       │
    │  commande-service:3003                      │
    │                                            │
    │  DNS interne : http://auth-service:3001    │
    │  Secrets injectes via App Settings          │
    └────────────────────────────────────────────┘
         |                      |
    Docker Hub            MongoDB (local ou Cosmos DB)
    (images sources)      (base de donnees)

    Surveillance : Log Stream + Diagnostics + Metriques
Dans docker-compose.azure.yml, commande-service utilise AUTH_SERVICE_URL: http://auth-service:3001. Pourquoi ce choix plutot que l'URL publique HTTPS de l'App Service ?
L'App Service retourne des 503 en permanence apres le deploiement. Citer 3 causes possibles et les commandes pour diagnostiquer.
Projet Final dariGo · Architecture · Plan
⏱ 10 min

dariGo — Plateforme de livraison de repas — Architecture

Presentation de dariGo (livraison de repas au Maroc), architecture 5 microservices + Nginx + RabbitMQ + MongoDB, tableau des responsabilites, creation de la structure des dossiers et initialisation npm.

ℹ Qu'est-ce que dariGo ?

dariGo est une plateforme de livraison de repas a domicile — pensee pour le marche marocain. Un client commande chez un restaurant, un livreur recoit la mission, les deux sont notifies en temps reel. Le nom vient de dari (maison en darija) et Go (livraison rapide).

Ce projet mobilise toutes les competences du module : API REST Node.js + MongoDB, microservices avec communication synchrone (axios) et asynchrone (RabbitMQ), reverse proxy Nginx, conteneurisation Docker Compose, et deploiement Azure App Service.


1
Architecture globale — 5 services + Nginx
Separation des responsabilites · Database per Service · RabbitMQ fanout
textArchitecture dariGo
CLIENT (Postman / Frontend)
        |
        | HTTP
        v
    NGINX :80  ← Gateway reverse proxy
    /auth      → auth-service:3001
    /cuisines  → cuisine-service:3002
    /commandes → commande-service:3003
    /livraisons→ livraison-service:3004

    RABBITMQ :5672  ← Broker de messages
    Exchange : darigo (fanout)
    ├── queue-notifs       → notif-service (consumer)
    └── queue-livreurs     → livraison-service (consumer)

    MONGODB
    ├── bddarigo-auth       (utilisateurs)
    ├── bddarigo-cuisines   (restaurants + plats)
    ├── bddarigo-commandes  (commandes)
    └── bddarigo-livraisons (missions livreurs)
ServicePortResponsabiliteBase
auth-service3001Register, login, /verify — JWT + bcryptbddarigo-auth
cuisine-service3002CRUD restaurants, CRUD plats, disponibilitebddarigo-cuisines
commande-service3003Creer commande, orchestrer auth+cuisine, publier evenement RabbitMQbddarigo-commandes
livraison-service3004Consumer RabbitMQ, affecter livreur, suivre statutbddarigo-livraisons
notif-service—Consumer RabbitMQ, enregistrer toutes les notificationsbddarigo-notifs
Nginx80Reverse proxy, routing, rate limiting sur /auth/login—

2
Structure des dossiers + initialisation
npm init · .env · shared/ · .gitignore
bashCreer la structure du projet
mkdir darigo && cd darigo

# Creer les dossiers de chaque service
mkdir auth-service cuisine-service commande-service livraison-service notif-service nginx shared

# Initialiser chaque service Node.js
for service in auth-service cuisine-service commande-service livraison-service notif-service; do
  cd $service
  npm init -y
  npm install express mongoose dotenv bcryptjs jsonwebtoken cors helmet
  cd ..
done

# commande-service a besoin d'axios pour appeler les autres services
cd commande-service && npm install axios amqplib && cd ..
cd livraison-service && npm install amqplib && cd ..
cd notif-service && npm install amqplib mongoose dotenv && cd ..
textdarigo/.env (a la racine — charge par docker-compose)
MONGO_URI=mongodb://localhost:27017
JWT_SECRET=darigo_jwt_secret_longue_cle_2024
JWT_EXPIRES_IN=7d
RABBITMQ_URL=amqp://guest:guest@localhost:5672
EXCHANGE_NOM=darigo
AUTH_PORT=3001
CUISINE_PORT=3002
COMMANDE_PORT=3003
LIVRAISON_PORT=3004
text.gitignore (racine + chaque service)
node_modules/
.env
.env.local
*.log
textStructure finale du projet
darigo/
├── auth-service/
│   ├── config/mongodb.js
│   ├── middlewares/isAuthenticated.js
│   ├── routes/auth.js
│   ├── .env  .dockerignore  Dockerfile
│   └── index.js
├── cuisine-service/
│   ├── config/mongodb.js
│   ├── routes/restaurants.js  routes/plats.js
│   └── index.js
├── commande-service/
│   ├── config/mongodb.js
│   ├── routes/commandes.js
│   └── index.js
├── livraison-service/
│   └── index.js  (consumer RabbitMQ + routes statut)
├── notif-service/
│   └── index.js  (consumer RabbitMQ pur)
├── shared/
│   ├── authClient.js    (verifierToken, isAuthenticated)
│   └── rabbitmq.js      (connecter, publierEvenement)
├── nginx/nginx.conf
├── docker-compose.yml
└── .env
Projet Final — Etape 1 auth-service · JWT · bcrypt · /verify
⏱ 20 min

Etape 1 — auth-service

Connexion MongoDB, POST /auth/register (bcrypt.hash), POST /auth/login (bcrypt.compare + jwt.sign), GET /auth/verify (endpoint inter-services), shared/authClient.js (isAuthenticated + isRole) reutilisable par tous les services.

✓ Ce que vous allez construire
  • Connexion MongoDB avec config/mongodb.js (connecter + getBase)
  • POST /auth/register : validation, hashage bcrypt, stockage
  • POST /auth/login : bcrypt.compare + jwt.sign
  • GET /auth/verify : endpoint inter-services pour valider un token sans partager JWT_SECRET
  • shared/authClient.js : middleware isAuthenticated reutilisable par tous les services

1
auth-service/config/mongodb.js + index.js
javascriptauth-service/config/mongodb.js
const { MongoClient } = require('mongodb');

const MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017';
const NOM_BASE  = process.env.NOM_BASE  || 'bddarigo-auth';

let base = null;

async function connecter() {
  const client = new MongoClient(MONGO_URI);
  await client.connect();
  base = client.db(NOM_BASE);
  console.log(`[auth-service] MongoDB connecte — base : ${NOM_BASE}`);
}

function getBase() {
  if (!base) throw new Error('MongoDB non connecte');
  return base;
}

module.exports = { connecter, getBase };
javascriptauth-service/index.js
require('dotenv').config();
const express = require('express');
const helmet  = require('helmet');
const cors    = require('cors');
const { connecter } = require('./config/mongodb');
const authRoutes    = require('./routes/auth');

const app  = express();
const PORT = process.env.PORT || 3001;

app.use(helmet());
app.use(cors());
app.use(express.json());

app.get('/health', (req, res) => res.json({ service: 'auth-service', statut: 'actif' }));
app.use('/auth', authRoutes);

app.use((err, req, res, next) => {
  console.error('[auth-service] Erreur :', err.message);
  res.status(500).json({ erreur: 'Erreur interne du serveur' });
});

connecter().then(() => {
  app.listen(PORT, () => console.log(`[auth-service] http://localhost:${PORT}`));
}).catch(err => {
  console.error('[auth-service] Connexion MongoDB echouee :', err.message);
  process.exit(1);
});

2
auth-service/routes/auth.js — register, login, verify
javascriptauth-service/routes/auth.js
const express = require('express');
const bcrypt  = require('bcryptjs');
const jwt     = require('jsonwebtoken');
const { getBase } = require('../config/mongodb');

const router = express.Router();

// ── POST /auth/register ───────────────────────────────────
router.post('/register', async (req, res) => {
  const { nom, email, motDePasse, role } = req.body;
  if (!nom || !email || !motDePasse)
    return res.status(400).json({ erreur: 'nom, email et motDePasse obligatoires' });

  const col = getBase().collection('utilisateurs');
  if (await col.findOne({ email }))
    return res.status(409).json({ erreur: 'Email deja utilise' });

  const hash = await bcrypt.hash(motDePasse, 12);
  const r = await col.insertOne({
    nom, email, motDePasse: hash,
    // role : 'client' | 'livreur' | 'restaurateur' | 'admin'
    role: role || 'client',
    createdAt: new Date()
  });
  res.status(201).json({ message: 'Compte cree', userId: r.insertedId });
});

// ── POST /auth/login ──────────────────────────────────────
router.post('/login', async (req, res) => {
  const { email, motDePasse } = req.body;
  if (!email || !motDePasse)
    return res.status(400).json({ erreur: 'email et motDePasse obligatoires' });

  const col  = getBase().collection('utilisateurs');
  const user = await col.findOne({ email });
  if (!user) return res.status(401).json({ erreur: 'Identifiants incorrects' });

  const ok = await bcrypt.compare(motDePasse, user.motDePasse);
  if (!ok) return res.status(401).json({ erreur: 'Identifiants incorrects' });

  const token = jwt.sign(
    { userId: user._id.toString(), nom: user.nom, email: user.email, role: user.role },
    process.env.JWT_SECRET,
    { expiresIn: process.env.JWT_EXPIRES_IN || '7d' }
  );
  res.json({ message: 'Connexion reussie', token });
});

// ── GET /auth/verify ── appele par les autres services ────
router.get('/verify', (req, res) => {
  const header = req.headers.authorization;
  if (!header) return res.status(401).json({ erreur: 'Token manquant' });

  const token = header.split(' ')[1];
  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);
    res.json({ userId: payload.userId, nom: payload.nom, email: payload.email, role: payload.role });
  } catch (err) {
    res.status(401).json({ erreur: err.name === 'TokenExpiredError' ? 'Token expire' : 'Token invalide' });
  }
});

module.exports = router;

3
shared/authClient.js — middleware partage
javascriptshared/authClient.js
const axios = require('axios');

const AUTH_URL = process.env.AUTH_SERVICE_URL || 'http://localhost:3001';

async function verifierToken(token) {
  try {
    const rep = await axios.get(`${AUTH_URL}/auth/verify`, {
      headers: { Authorization: `Bearer ${token}` },
      timeout: 5000
    });
    return rep.data; // { userId, nom, email, role }
  } catch (err) {
    if (err.response)
      throw { statut: err.response.status, message: err.response.data.erreur };
    throw { statut: 503, message: 'auth-service indisponible' };
  }
}

function isAuthenticated(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ erreur: 'Token manquant — Authorization: Bearer token' });
  verifierToken(token)
    .then(payload => { req.utilisateur = payload; next(); })
    .catch(err => res.status(err.statut || 401).json({ erreur: err.message }));
}

function isRole(...roles) {
  return (req, res, next) => {
    if (!req.utilisateur || !roles.includes(req.utilisateur.role))
      return res.status(403).json({ erreur: `Acces reserve aux roles : ${roles.join(', ')}` });
    next();
  };
}

module.exports = { verifierToken, isAuthenticated, isRole };
✓ Tests Postman — auth-service
  • POST /auth/register { "nom":"Karim","email":"karim@darigo.ma","motDePasse":"Karim2024","role":"client" } → 201
  • POST /auth/register (email deja utilise) → 409
  • POST /auth/login { "email":"karim@darigo.ma","motDePasse":"Karim2024" } → 200 + token (copier)
  • POST /auth/login (mauvais mot de passe) → 401, meme message que email inexistant
  • GET /auth/verify avec Authorization: Bearer <token> → 200 + payload
  • GET /auth/verify sans token → 401

✓
Verification des acquis
Pourquoi auth-service expose-t-il GET /auth/verify au lieu de partager JWT_SECRET avec tous les services ?
Projet Final — Etape 2 cuisine-service · restaurants · plats
⏱ 22 min

Etape 2 — cuisine-service

CRUD restaurants (GET public avec pagination/filtre, POST restaurateur, PUT, DELETE admin), CRUD plats par restaurant (GET avec champ disponible calcule, POST, PUT), protection isAuthenticated + isRole depuis shared/authClient.js.

✓ Ce que vous allez construire
  • CRUD complet des restaurants : GET liste, GET :id, POST (restaurateur), PUT, DELETE (admin)
  • CRUD des plats par restaurant : GET /restaurants/:id/plats, POST /restaurants/:id/plats
  • Champ calcule disponible sur chaque plat selon stock > 0
  • Pagination skip/limit sur la liste des restaurants
  • Protection des routes avec isAuthenticated et isRole depuis shared/

1
cuisine-service/index.js + config
javascriptcuisine-service/index.js
require('dotenv').config();
const express = require('express');
const helmet  = require('helmet');
const cors    = require('cors');
const { connecter } = require('./config/mongodb');
const restaurantsRoutes = require('./routes/restaurants');
const platsRoutes       = require('./routes/plats');

const app  = express();
const PORT = process.env.PORT || 3002;

app.use(helmet()); app.use(cors()); app.use(express.json());

app.get('/health', (req, res) => res.json({ service: 'cuisine-service', statut: 'actif' }));
app.use('/restaurants', restaurantsRoutes);
app.use('/restaurants',  platsRoutes);  // /restaurants/:id/plats

app.use((err, req, res, next) => {
  console.error('[cuisine-service] :', err.message);
  res.status(500).json({ erreur: 'Erreur interne' });
});

connecter().then(() =>
  app.listen(PORT, () => console.log(`[cuisine-service] http://localhost:${PORT}`))
).catch(err => { console.error(err.message); process.exit(1); });

2
routes/restaurants.js — CRUD complet
javascriptcuisine-service/routes/restaurants.js
const express = require('express');
const { ObjectId } = require('mongodb');
const { getBase } = require('../config/mongodb');
const { isAuthenticated, isRole } = require('../../shared/authClient');

const router = express.Router();

// ── GET /restaurants — liste publique avec pagination ─────
router.get('/', async (req, res) => {
  const { ville, categorie, page = 1, limite = 10 } = req.query;
  const filtre = {};
  if (ville)      filtre.ville = new RegExp(ville, 'i');  // recherche insensible a la casse
  if (categorie)  filtre.categorie = categorie;

  const skip  = (parseInt(page) - 1) * parseInt(limite);
  const col   = getBase().collection('restaurants');
  const total = await col.countDocuments(filtre);
  const restaurants = await col.find(filtre)
    .sort({ note: -1 })               // trier par note decroissante
    .skip(skip).limit(parseInt(limite))
    .toArray();

  res.json({ total, page: parseInt(page), pages: Math.ceil(total / parseInt(limite)), restaurants });
});

// ── GET /restaurants/:id ──────────────────────────────────
router.get('/:id', async (req, res) => {
  if (!ObjectId.isValid(req.params.id))
    return res.status(400).json({ erreur: 'Id invalide' });

  const restaurant = await getBase().collection('restaurants')
    .findOne({ _id: new ObjectId(req.params.id) });
  if (!restaurant) return res.status(404).json({ erreur: 'Restaurant introuvable' });
  res.json({ restaurant });
});

// ── POST /restaurants — restaurateur ou admin uniquement ──
router.post('/', isAuthenticated, isRole('restaurateur', 'admin'), async (req, res) => {
  const { nom, ville, adresse, telephone, categorie, horaires } = req.body;
  if (!nom || !ville || !adresse || !categorie)
    return res.status(400).json({ erreur: 'nom, ville, adresse et categorie obligatoires' });

  const restaurant = {
    nom, ville, adresse, telephone: telephone || null,
    categorie,  // 'tajine' | 'couscous' | 'pizza' | 'burger' | 'sushi' ...
    horaires: horaires || null,
    note: 0,      // moyenne des avis
    nbAvis: 0,
    proprietaireId: req.utilisateur.userId,
    actif: true,
    createdAt: new Date()
  };
  const r = await getBase().collection('restaurants').insertOne(restaurant);
  res.status(201).json({ message: 'Restaurant cree', restaurantId: r.insertedId });
});

// ── PUT /restaurants/:id ──────────────────────────────────
router.put('/:id', isAuthenticated, isRole('restaurateur', 'admin'), async (req, res) => {
  if (!ObjectId.isValid(req.params.id))
    return res.status(400).json({ erreur: 'Id invalide' });

  const { nom, ville, adresse, telephone, categorie, horaires, actif } = req.body;
  const mise = {};
  if (nom !== undefined)       mise.nom = nom;
  if (ville !== undefined)     mise.ville = ville;
  if (adresse !== undefined)   mise.adresse = adresse;
  if (telephone !== undefined) mise.telephone = telephone;
  if (categorie !== undefined) mise.categorie = categorie;
  if (horaires !== undefined)  mise.horaires = horaires;
  if (actif !== undefined)     mise.actif = actif;
  mise.updatedAt = new Date();

  const r = await getBase().collection('restaurants')
    .updateOne({ _id: new ObjectId(req.params.id) }, { $set: mise });
  if (r.matchedCount === 0) return res.status(404).json({ erreur: 'Restaurant introuvable' });
  res.json({ message: 'Restaurant mis a jour' });
});

// ── DELETE /restaurants/:id — admin uniquement ────────────
router.delete('/:id', isAuthenticated, isRole('admin'), async (req, res) => {
  if (!ObjectId.isValid(req.params.id))
    return res.status(400).json({ erreur: 'Id invalide' });

  const r = await getBase().collection('restaurants')
    .deleteOne({ _id: new ObjectId(req.params.id) });
  if (r.deletedCount === 0) return res.status(404).json({ erreur: 'Restaurant introuvable' });
  res.json({ message: 'Restaurant supprime' });
});

module.exports = router;

3
routes/plats.js — CRUD plats par restaurant
javascriptcuisine-service/routes/plats.js
const express = require('express');
const { ObjectId } = require('mongodb');
const { getBase } = require('../config/mongodb');
const { isAuthenticated, isRole } = require('../../shared/authClient');

const router = express.Router();

// ── GET /restaurants/:id/plats — liste publique ───────────
router.get('/:restaurantId/plats', async (req, res) => {
  if (!ObjectId.isValid(req.params.restaurantId))
    return res.status(400).json({ erreur: 'restaurantId invalide' });

  const plats = await getBase().collection('plats')
    .find({ restaurantId: req.params.restaurantId })
    .toArray();

  // Champ calcule : disponible selon le stock
  const platsAvecDispo = plats.map(p => ({ ...p, disponible: p.stock > 0 }));
  res.json({ restaurantId: req.params.restaurantId, plats: platsAvecDispo });
});

// ── POST /restaurants/:id/plats ───────────────────────────
router.post('/:restaurantId/plats', isAuthenticated, isRole('restaurateur', 'admin'), async (req, res) => {
  const { nom, description, prix, categorie, stock } = req.body;
  if (!nom || prix === undefined)
    return res.status(400).json({ erreur: 'nom et prix obligatoires' });

  const plat = {
    restaurantId: req.params.restaurantId,
    nom, description: description || '',
    prix: parseFloat(prix),
    categorie: categorie || 'plat',  // 'entree' | 'plat' | 'dessert' | 'boisson'
    stock: stock !== undefined ? parseInt(stock) : 999,
    actif: true,
    createdAt: new Date()
  };
  const r = await getBase().collection('plats').insertOne(plat);
  res.status(201).json({ message: 'Plat ajoute', platId: r.insertedId });
});

// ── PUT /restaurants/:rid/plats/:pid — modifier un plat ───
router.put('/:restaurantId/plats/:platId', isAuthenticated, isRole('restaurateur', 'admin'), async (req, res) => {
  if (!ObjectId.isValid(req.params.platId))
    return res.status(400).json({ erreur: 'platId invalide' });

  const { nom, description, prix, stock, actif } = req.body;
  const mise = {};
  if (nom !== undefined)         mise.nom = nom;
  if (description !== undefined) mise.description = description;
  if (prix !== undefined)        mise.prix = parseFloat(prix);
  if (stock !== undefined)       mise.stock = parseInt(stock);
  if (actif !== undefined)       mise.actif = actif;
  mise.updatedAt = new Date();

  const r = await getBase().collection('plats')
    .updateOne({ _id: new ObjectId(req.params.platId) }, { $set: mise });
  if (r.matchedCount === 0) return res.status(404).json({ erreur: 'Plat introuvable' });
  res.json({ message: 'Plat mis a jour' });
});

module.exports = router;
✓ Tests Postman — cuisine-service
  • POST /auth/register avec role restaurateur → token_restaurateur
  • POST /restaurants avec token_restaurateur → 201 + restaurantId
  • POST /restaurants/:id/plats → ajouter 3 plats avec des prix differents
  • GET /restaurants/:id/plats → liste avec champ disponible
  • GET /restaurants?ville=Casablanca → filtre par ville
  • DELETE /restaurants/:id avec token_client → 403
Projet Final — Etape 3 commande-service · axios · RabbitMQ
⏱ 22 min

Etape 3 — commande-service

shared/rabbitmq.js (singleton + reconnexion auto), POST /commandes (orchestration auth+cuisine, validation stock, denormalisation, publication RabbitMQ), GET /commandes, PATCH /commandes/:id/statut — reponse immediate au client sans attendre les consumers.

✓ Ce que vous allez construire
  • POST /commandes : orchestrer auth + cuisine, valider stock, creer commande, publier evenement RabbitMQ
  • GET /commandes : liste des commandes de l'utilisateur connecte
  • GET /commandes/:id : detail d'une commande
  • PATCH /commandes/:id/statut : mettre a jour le statut
  • shared/rabbitmq.js : module singleton de publication

1
shared/rabbitmq.js — module de publication
javascriptshared/rabbitmq.js
const amqplib = require('amqplib');

const RABBITMQ_URL = process.env.RABBITMQ_URL || 'amqp://guest:guest@localhost:5672';
const EXCHANGE_NOM = process.env.EXCHANGE_NOM  || 'darigo';

let channel = null;

async function connecterRabbitMQ() {
  try {
    const connexion = await amqplib.connect(RABBITMQ_URL);
    channel = await connexion.createChannel();
    await channel.assertExchange(EXCHANGE_NOM, 'fanout', { durable: true });
    console.log(`[RabbitMQ] Connecte — exchange : ${EXCHANGE_NOM}`);

    connexion.on('close', () => {
      console.warn('[RabbitMQ] Connexion perdue — reconnexion dans 5s');
      channel = null;
      setTimeout(connecterRabbitMQ, 5000);
    });
  } catch (err) {
    console.warn('[RabbitMQ] Indisponible :', err.message, '— retry dans 5s');
    setTimeout(connecterRabbitMQ, 5000);
  }
}

async function publierEvenement(evenement) {
  if (!channel) {
    console.warn('[RabbitMQ] Pas de channel — evenement ignore :', evenement.type);
    return false;
  }
  try {
    const buffer = Buffer.from(JSON.stringify(evenement));
    channel.publish(EXCHANGE_NOM, '', buffer, { persistent: true });
    console.log('[RabbitMQ] Evenement publie :', evenement.type, evenement.commandeId);
    return true;
  } catch (err) {
    console.warn('[RabbitMQ] Erreur publication :', err.message);
    return false;
  }
}

module.exports = { connecterRabbitMQ, publierEvenement };

2
commande-service/routes/commandes.js
javascriptcommande-service/routes/commandes.js
const express = require('express');
const { ObjectId } = require('mongodb');
const axios   = require('axios');
const { getBase } = require('../config/mongodb');
const { isAuthenticated } = require('../../shared/authClient');
const { publierEvenement } = require('../../shared/rabbitmq');

const router = express.Router();

const CUISINE_URL = process.env.CUISINE_SERVICE_URL || 'http://localhost:3002';

// ── POST /commandes ───────────────────────────────────────
router.post('/', isAuthenticated, async (req, res) => {
  const { restaurantId, lignes } = req.body;
  // lignes = [{ platId, quantite }, ...]
  if (!restaurantId || !lignes || !lignes.length)
    return res.status(400).json({ erreur: 'restaurantId et lignes obligatoires' });

  // 1 — Verifier chaque plat + calculer le total
  let total = 0;
  const lignesDetail = [];

  for (const ligne of lignes) {
    const { platId, quantite } = ligne;
    if (!quantite || quantite < 1)
      return res.status(400).json({ erreur: `Quantite invalide pour le plat ${platId}` });

    let platData;
    try {
      const rep = await axios.get(
        `${CUISINE_URL}/restaurants/${restaurantId}/plats`,
        { timeout: 5000 }
      );
      platData = rep.data.plats.find(p => p._id.toString() === platId);
    } catch (err) {
      const statut = err.response ? err.response.status : 503;
      return res.status(statut).json({ erreur: 'cuisine-service indisponible ou restaurant introuvable' });
    }

    if (!platData)
      return res.status(404).json({ erreur: `Plat ${platId} introuvable dans ce restaurant` });
    if (!platData.disponible)
      return res.status(409).json({ erreur: `Plat "${platData.nom}" hors stock` });

    total += platData.prix * quantite;
    lignesDetail.push({
      platId,
      nomPlat: platData.nom,     // Denormalisation — snapshot au moment de la commande
      prixUnitaire: platData.prix,
      quantite,
      sousTotal: platData.prix * quantite
    });
  }

  // 2 — Creer la commande
  const commande = {
    clientId:     req.utilisateur.userId,
    nomClient:    req.utilisateur.nom,
    restaurantId,
    lignes:       lignesDetail,
    total:        Math.round(total * 100) / 100,  // arrondir a 2 decimales
    statut:       'en_attente',  // en_attente | confirmee | en_preparation | prete | en_livraison | livree | annulee
    livreurId:    null,
    createdAt:    new Date(),
    updatedAt:    new Date()
  };

  const r = await getBase().collection('commandes').insertOne(commande);
  const commandeId = r.insertedId.toString();

  // 3 — Publier l'evenement (ne bloque pas la reponse)
  await publierEvenement({
    type:        'commande.creee',
    commandeId,
    clientId:    req.utilisateur.userId,
    nomClient:   req.utilisateur.nom,
    restaurantId,
    total:       commande.total,
    lignes:      lignesDetail,
    timestamp:   new Date().toISOString()
  });

  // 4 — Repondre immediatement — sans attendre les consumers
  res.status(201).json({
    message:   'Commande creee',
    commandeId,
    total:     commande.total,
    statut:    commande.statut
  });
});

// ── GET /commandes — commandes du client connecte ─────────
router.get('/', isAuthenticated, async (req, res) => {
  const { statut } = req.query;
  const filtre = { clientId: req.utilisateur.userId };
  if (statut) filtre.statut = statut;

  const commandes = await getBase().collection('commandes')
    .find(filtre).sort({ createdAt: -1 }).toArray();
  res.json({ total: commandes.length, commandes });
});

// ── GET /commandes/:id ────────────────────────────────────
router.get('/:id', isAuthenticated, async (req, res) => {
  if (!ObjectId.isValid(req.params.id))
    return res.status(400).json({ erreur: 'Id invalide' });

  const commande = await getBase().collection('commandes')
    .findOne({ _id: new ObjectId(req.params.id) });
  if (!commande) return res.status(404).json({ erreur: 'Commande introuvable' });
  if (commande.clientId !== req.utilisateur.userId &&
      req.utilisateur.role !== 'admin')
    return res.status(403).json({ erreur: 'Acces non autorise' });

  res.json({ commande });
});

// ── PATCH /commandes/:id/statut ───────────────────────────
router.patch('/:id/statut', isAuthenticated, async (req, res) => {
  if (!ObjectId.isValid(req.params.id))
    return res.status(400).json({ erreur: 'Id invalide' });

  const { statut } = req.body;
  const statutsValides = ['confirmee','en_preparation','prete','en_livraison','livree','annulee'];
  if (!statutsValides.includes(statut))
    return res.status(400).json({ erreur: `Statut invalide. Valides : ${statutsValides.join(', ')}` });

  const r = await getBase().collection('commandes')
    .updateOne(
      { _id: new ObjectId(req.params.id) },
      { $set: { statut, updatedAt: new Date() } }
    );
  if (r.matchedCount === 0) return res.status(404).json({ erreur: 'Commande introuvable' });
  res.json({ message: `Statut mis a jour : ${statut}` });
});

module.exports = router;
javascriptcommande-service/index.js
require('dotenv').config();
const express = require('express');
const helmet  = require('helmet');
const cors    = require('cors');
const { connecter }         = require('./config/mongodb');
const { connecterRabbitMQ } = require('../shared/rabbitmq');
const commandesRoutes       = require('./routes/commandes');

const app  = express();
const PORT = process.env.PORT || 3003;

app.use(helmet()); app.use(cors()); app.use(express.json());

app.get('/health', (req, res) => res.json({ service: 'commande-service', statut: 'actif' }));
app.use('/commandes', commandesRoutes);

connecter().then(() => {
  connecterRabbitMQ(); // connexion asynchrone — ne bloque pas le demarrage
  app.listen(PORT, () => console.log(`[commande-service] http://localhost:${PORT}`));
}).catch(err => { console.error(err.message); process.exit(1); });

✓
Verification des acquis
La commande enregistre nomPlat et prixUnitaire directement dans le document. Pourquoi ne pas stocker uniquement platId et lire cuisine-service a chaque consultation ?
Projet Final — Etape 4 livraison-service · consumer · ack
⏱ 20 min

Etape 4 — livraison-service

Consumer RabbitMQ (ecoute commande.creee, cree mission disponible, ack/nack), GET /livraisons (missions selon role), PATCH /accepter (livreur accepte une mission — statut atomique), PATCH /statut (en_route, livree), reconnexion automatique RabbitMQ.

✓ Ce que vous allez construire
  • Consumer RabbitMQ : ecouter l'evenement commande.creee et creer une mission de livraison
  • GET /livraisons : liste des missions du livreur connecte
  • PATCH /livraisons/:id/accepter : un livreur accepte une mission
  • PATCH /livraisons/:id/statut : le livreur met a jour le statut (en_route, livree)

1
livraison-service/index.js — consumer + routes
javascriptlivraison-service/index.js
require('dotenv').config();
const express  = require('express');
const amqplib  = require('amqplib');
const { ObjectId } = require('mongodb');
const helmet   = require('helmet');
const cors     = require('cors');
const { connecter, getBase } = require('./config/mongodb');
const { isAuthenticated, isRole } = require('../shared/authClient');

const app  = express();
const PORT = process.env.PORT || 3004;

const RABBITMQ_URL = process.env.RABBITMQ_URL || 'amqp://guest:guest@localhost:5672';
const EXCHANGE_NOM = process.env.EXCHANGE_NOM  || 'darigo';
const QUEUE_NOM    = 'queue-livraisons';

app.use(helmet()); app.use(cors()); app.use(express.json());
app.get('/health', (req, res) => res.json({ service: 'livraison-service', statut: 'actif' }));

// ── Consumer RabbitMQ ─────────────────────────────────────
async function demarrerConsumer() {
  try {
    const connexion = await amqplib.connect(RABBITMQ_URL);
    const channel   = await connexion.createChannel();
    await channel.assertExchange(EXCHANGE_NOM, 'fanout', { durable: true });
    await channel.assertQueue(QUEUE_NOM, { durable: true });
    await channel.bindQueue(QUEUE_NOM, EXCHANGE_NOM, '');
    console.log(`[livraison-service] Consumer actif — ecoute ${QUEUE_NOM}`);

    channel.consume(QUEUE_NOM, async (msg) => {
      if (!msg) return;
      try {
        const evt = JSON.parse(msg.content.toString());
        if (evt.type !== 'commande.creee') { channel.ack(msg); return; }

        // Creer une mission de livraison en statut "disponible"
        await getBase().collection('livraisons').insertOne({
          commandeId:   evt.commandeId,
          clientId:     evt.clientId,
          nomClient:    evt.nomClient,
          restaurantId: evt.restaurantId,
          total:        evt.total,
          livreurId:    null,   // Pas encore affecte
          statut:       'disponible',  // disponible | acceptee | en_route | livree | annulee
          createdAt:    new Date()
        });
        console.log(`[livraison-service] Mission creee pour commande ${evt.commandeId}`);
        channel.ack(msg);
      } catch (err) {
        console.error('[livraison-service] Erreur traitement :', err.message);
        channel.nack(msg, false, false); // ne pas remettre en queue (eviter boucle)
      }
    }, { noAck: false });

    connexion.on('close', () => {
      console.warn('[livraison-service] RabbitMQ deconnecte — retry 5s');
      setTimeout(demarrerConsumer, 5000);
    });
  } catch (err) {
    console.warn('[livraison-service] RabbitMQ indisponible :', err.message, '— retry 5s');
    setTimeout(demarrerConsumer, 5000);
  }
}

// ── GET /livraisons — missions disponibles ou du livreur ──
app.get('/livraisons', isAuthenticated, async (req, res) => {
  const { statut } = req.query;
  const filtre = {};

  if (req.utilisateur.role === 'livreur') {
    // Un livreur voit ses missions acceptees + les missions disponibles
    if (statut) {
      filtre.statut = statut;
    } else {
      filtre.$or = [
        { statut: 'disponible' },
        { livreurId: req.utilisateur.userId }
      ];
    }
  } else if (req.utilisateur.role === 'admin') {
    if (statut) filtre.statut = statut;
  } else {
    // Client : uniquement ses livraisons
    filtre.clientId = req.utilisateur.userId;
  }

  const livraisons = await getBase().collection('livraisons')
    .find(filtre).sort({ createdAt: -1 }).toArray();
  res.json({ total: livraisons.length, livraisons });
});

// ── PATCH /livraisons/:id/accepter — livreur accepte ─────
app.patch('/livraisons/:id/accepter', isAuthenticated, isRole('livreur'), async (req, res) => {
  if (!ObjectId.isValid(req.params.id))
    return res.status(400).json({ erreur: 'Id invalide' });

  const r = await getBase().collection('livraisons').updateOne(
    { _id: new ObjectId(req.params.id), statut: 'disponible' },
    { $set: { livreurId: req.utilisateur.userId, statut: 'acceptee', updatedAt: new Date() } }
  );
  if (r.matchedCount === 0)
    return res.status(409).json({ erreur: 'Mission indisponible ou deja acceptee' });
  res.json({ message: 'Mission acceptee' });
});

// ── PATCH /livraisons/:id/statut — livreur met a jour ─────
app.patch('/livraisons/:id/statut', isAuthenticated, isRole('livreur', 'admin'), async (req, res) => {
  if (!ObjectId.isValid(req.params.id))
    return res.status(400).json({ erreur: 'Id invalide' });

  const { statut } = req.body;
  const valides = ['en_route', 'livree', 'annulee'];
  if (!valides.includes(statut))
    return res.status(400).json({ erreur: `Statut invalide. Valides : ${valides.join(', ')}` });

  const r = await getBase().collection('livraisons').updateOne(
    { _id: new ObjectId(req.params.id) },
    { $set: { statut, updatedAt: new Date() } }
  );
  if (r.matchedCount === 0) return res.status(404).json({ erreur: 'Mission introuvable' });
  res.json({ message: `Statut mis a jour : ${statut}` });
});

connecter().then(() => {
  demarrerConsumer();
  app.listen(PORT, () => console.log(`[livraison-service] http://localhost:${PORT}`));
}).catch(err => { console.error(err.message); process.exit(1); });
Projet Final — Etape 5 notif-service · consumer pur · resilience
⏱ 18 min

Etape 5 — notif-service

Consumer RabbitMQ pur (ecoute tous les evenements exchange darigo, enregistre dans MongoDB), GET /notifs (historique admin), demonstration de resilience : messages en attente pendant panne, consommes au redemarrage.

✓ Ce que vous allez construire
  • Consumer RabbitMQ pur : ecouter TOUS les evenements de l'exchange darigo
  • Enregistrer chaque evenement dans MongoDB (bddarigo-notifs) avec horodatage
  • GET /notifs : consulter l'historique des notifications (admin uniquement)
  • Demonstration de la resilience : arreter notif-service, creer des commandes, relancer → tous les evenements sont consommes

1
notif-service/index.js
javascriptnotif-service/index.js
require('dotenv').config();
const express  = require('express');
const amqplib  = require('amqplib');
const { MongoClient } = require('mongodb');
const { isAuthenticated, isRole } = require('../shared/authClient');

const RABBITMQ_URL = process.env.RABBITMQ_URL || 'amqp://guest:guest@localhost:5672';
const EXCHANGE_NOM = process.env.EXCHANGE_NOM  || 'darigo';
const MONGO_URI    = process.env.MONGO_URI     || 'mongodb://localhost:27017';
const NOM_BASE     = process.env.NOM_BASE      || 'bddarigo-notifs';
const QUEUE_NOM    = 'queue-notifs';
const PORT         = process.env.PORT || 3005;

const app = express();
app.use(express.json());
let db = null;

// ── Connexion MongoDB ─────────────────────────────────────
async function connecterMongo() {
  const client = new MongoClient(MONGO_URI);
  await client.connect();
  db = client.db(NOM_BASE);
  console.log('[notif-service] MongoDB connecte');
}

// ── Consumer RabbitMQ ─────────────────────────────────────
async function demarrerConsumer() {
  try {
    const connexion = await amqplib.connect(RABBITMQ_URL);
    const channel   = await connexion.createChannel();
    await channel.assertExchange(EXCHANGE_NOM, 'fanout', { durable: true });
    await channel.assertQueue(QUEUE_NOM, { durable: true });
    await channel.bindQueue(QUEUE_NOM, EXCHANGE_NOM, '');
    console.log(`[notif-service] Consumer actif — ecoute tous les evenements darigo`);

    channel.consume(QUEUE_NOM, async (msg) => {
      if (!msg) return;
      try {
        const evt = JSON.parse(msg.content.toString());

        // Enregistrer l'evenement — peu importe son type
        await db.collection('evenements').insertOne({
          ...evt,
          recu_le: new Date()
        });

        // Simuler une notification selon le type
        switch (evt.type) {
          case 'commande.creee':
            console.log(`[notif-service] SMS envoye a ${evt.nomClient} — commande ${evt.commandeId} (${evt.total} MAD)`);
            break;
          default:
            console.log(`[notif-service] Evenement enregistre : ${evt.type}`);
        }

        channel.ack(msg);
      } catch (err) {
        console.error('[notif-service] Erreur :', err.message);
        channel.nack(msg, false, false);
      }
    }, { noAck: false });

    connexion.on('close', () => {
      console.warn('[notif-service] RabbitMQ deconnecte — retry 5s');
      setTimeout(demarrerConsumer, 5000);
    });
  } catch (err) {
    console.warn('[notif-service] RabbitMQ indisponible :', err.message, '— retry 5s');
    setTimeout(demarrerConsumer, 5000);
  }
}

// ── GET /notifs — historique admin ────────────────────────
app.get('/health', (req, res) => res.json({ service: 'notif-service', statut: 'actif' }));

app.get('/notifs', isAuthenticated, isRole('admin'), async (req, res) => {
  const { type, page = 1, limite = 20 } = req.query;
  const filtre = {};
  if (type) filtre.type = type;

  const skip  = (parseInt(page) - 1) * parseInt(limite);
  const total = await db.collection('evenements').countDocuments(filtre);
  const notifs = await db.collection('evenements')
    .find(filtre).sort({ recu_le: -1 }).skip(skip).limit(parseInt(limite)).toArray();

  res.json({ total, page: parseInt(page), notifs });
});

// ── Demarrage ─────────────────────────────────────────────
connecterMongo().then(() => {
  demarrerConsumer();
  app.listen(PORT, () => console.log(`[notif-service] http://localhost:${PORT}`));
}).catch(err => { console.error(err.message); process.exit(1); });

2
Test de resilience RabbitMQ
Arreter le consumer · Creer des commandes · Relancer · Aucun message perdu
bashScenario de resilience — executer etape par etape
# Etape 1 : arreter notif-service (simuler une panne)
Ctrl+C dans le terminal notif-service

# Etape 2 : creer 3 commandes via Postman (POST /commandes)
# Les 201 arrivent normalement — commande-service ne depend pas de notif-service

# Etape 3 : verifier l'interface RabbitMQ
# http://localhost:15672 → queue-notifs → Messages Ready: 3

# Etape 4 : relancer notif-service
node notif-service/index.js

# Observer dans les logs : 3 messages consommes immediatement
# [notif-service] SMS envoye a Karim — commande cmd-xxx
# [notif-service] SMS envoye a Nadia — commande cmd-yyy
# [notif-service] SMS envoye a Youssef — commande cmd-zzz

# Etape 5 : verifier que les evenements sont dans MongoDB
# GET /notifs (admin token) → total: 3
Projet Final — Etape 6 Nginx · rate limiting · proxy_pass
⏱ 16 min

Etape 6 — Nginx Gateway

nginx.conf avec 4 upstreams, rate limiting anti-brute-force sur /auth/login (10r/min), transmission Authorization header, rerouting /cuisines/ → /restaurants/, page d'erreur JSON 502/503/504, test isolation (client sur port 80 uniquement).

✓ Ce que vous allez configurer
  • nginx.conf complet avec 4 upstreams (auth, cuisine, commande, livraison)
  • Rate limiting sur /auth/login (10 req/min) pour bloquer le brute force
  • Transmission du header Authorization vers les backends
  • Page d'erreur JSON 502/503/504 (plus de HTML d'erreur Nginx)
  • Test de l'isolation : le client n'accede qu'a localhost:80

1
nginx/nginx.conf — configuration complete
nginxnginx/nginx.conf
events {
  worker_connections 1024;
}

http {
  # ── Rate limiting : 10 requetes/minute par IP sur /auth/login ──
  limit_req_zone $binary_remote_addr zone=auth_login:10m rate=10r/m;

  # ── Upstreams — noms des services docker-compose ────────────
  upstream auth_service      { server auth-service:3001; }
  upstream cuisine_service   { server cuisine-service:3002; }
  upstream commande_service  { server commande-service:3003; }
  upstream livraison_service { server livraison-service:3004; }

  server {
    listen 80;
    server_name localhost;

    # Headers transmis aux services backend
    proxy_set_header Host              $host;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header Authorization     $http_authorization;  # CRITIQUE : transmettre le JWT

    proxy_connect_timeout  5s;
    proxy_read_timeout    30s;

    # ── Routes — ordre : du plus specifique au plus general ──
    location /auth/login {
      limit_req zone=auth_login burst=5 nodelay;  # max 5 en rafale
      proxy_pass http://auth_service/auth/login;
    }

    location /auth/      { proxy_pass http://auth_service/auth/; }
    location /cuisines/  { proxy_pass http://cuisine_service/restaurants/; }
    location /commandes/ { proxy_pass http://commande_service/commandes/; }
    location /livraisons/{ proxy_pass http://livraison_service/livraisons/; }

    # Health check Nginx
    location /nginx-health {
      return 200 '{"service":"nginx","statut":"actif"}';
      add_header Content-Type application/json;
    }

    # Page d'erreur JSON quand un service backend est hors ligne
    error_page 502 503 504 /erreur_service.json;
    location = /erreur_service.json {
      return 503 '{"erreur":"Service temporairement indisponible"}';
      add_header Content-Type application/json;
    }

    # Bloquer les acces directs aux endpoints internes
    location /notifs/ { return 403 '{"erreur":"Acces direct interdit"}'; }
  }
}
ℹ Rerouting de /cuisines/ vers /restaurants/

proxy_pass http://cuisine_service/restaurants/ signifie que le client appelle /cuisines/abc et Nginx appelle cuisine-service avec /restaurants/abc. L'URL publique est semantique (/cuisines), l'URL interne est technique (/restaurants). Le client ne connait jamais l'URL interne des services.

⚠ Tester la syntaxe avant chaque reload
sudo nginx -t        # Tester la syntaxe — TOUJOURS avant
sudo nginx -s reload # Recharger sans downtime
✓ Tests via Nginx (port 80 uniquement)
  • GET http://localhost/nginx-health → 200
  • POST http://localhost/auth/register → transmet vers auth-service:3001
  • GET http://localhost/cuisines/ → liste des restaurants (cuisine-service:3002/restaurants/)
  • POST http://localhost/commandes/ avec token → commande creee
  • Arreter auth-service → POST /auth/login → 503 JSON (pas le HTML Nginx)
  • Envoyer 15 POST /auth/login en 1 minute → 429 apres la 10eme
Projet Final — Etape 7 Docker Compose · Reseaux · healthcheck
⏱ 20 min

Etape 7 — Dockerisation complete

Dockerfile commun (node:18-alpine, contexte racine pour shared/), .dockerignore, docker-compose.yml 7 services (MongoDB + RabbitMQ + 5 Node.js + Nginx), reseaux multi-couches (darigo-frontend / darigo-backend), healthchecks, 20 tests de validation.

✓ Ce que vous allez construire
  • Dockerfile identique pour les 5 services Node.js (node:18-alpine, ordre cache optimal)
  • .dockerignore pour exclure node_modules et .env
  • docker-compose.yml complet : 7 services, reseaux multi-couches, healthchecks, .env
  • Reseaux : darigo-frontend (Nginx + services) et darigo-backend (services + MongoDB + RabbitMQ)
  • Validation : 20 tests de demarrage et flux complet

1
Dockerfile — template commun aux 5 services
dockerfileauth-service/Dockerfile (identique pour cuisine, commande, livraison, notif — adapter PORT et nom)
FROM node:18-alpine

WORKDIR /app

# Copier les manifestes AVANT le code source → npm install en cache
COPY package*.json ./
RUN npm install --production

# Copier le code source + le dossier shared/
COPY . .

ENV NODE_ENV=production
ENV PORT=3001

EXPOSE 3001

CMD ["node", "index.js"]
⚠ Le dossier shared/ doit etre accessible depuis chaque service

Chaque service importe ../../shared/authClient.js. Dans Docker, le contexte de build est le dossier du service — le dossier shared/ est un niveau au-dessus et n'est pas copie par COPY . .. Solution dans docker-compose : utiliser le contexte de build a la racine du projet.

textauth-service/.dockerignore (identique pour chaque service)
node_modules
.env
.env.*
*.log
.git
README.md

2
docker-compose.yml — architecture complete
yamldarigo/docker-compose.yml
version: '3.9'

services:

  # ── Infrastructure ──────────────────────────────────────
  mongodb:
    image: mongo:7.0
    container_name: darigo-mongodb
    ports:
      - "27017:27017"
    environment:
      MONGO_INITDB_ROOT_USERNAME: ${MONGO_USER:-admin}
      MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD:-secret}
    volumes:
      - mongo_data:/data/db
    networks:
      - darigo-backend
    healthcheck:
      test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
      interval: 5s
      retries: 5
      start_period: 10s

  rabbitmq:
    image: rabbitmq:3-management
    container_name: darigo-rabbitmq
    ports:
      - "15672:15672"    # Interface web
    environment:
      RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-guest}
      RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASS:-guest}
    volumes:
      - rabbitmq_data:/var/lib/rabbitmq
    networks:
      - darigo-backend
    healthcheck:
      test: ["CMD", "rabbitmq-diagnostics", "ping"]
      interval: 10s
      retries: 5
      start_period: 30s

  # ── Services Node.js ─────────────────────────────────────
  auth-service:
    build:
      context: .                  # Racine du projet (pour inclure shared/)
      dockerfile: auth-service/Dockerfile
    container_name: darigo-auth
    environment:
      PORT: 3001
      MONGO_URI: mongodb://${MONGO_USER:-admin}:${MONGO_PASSWORD:-secret}@mongodb:27017
      NOM_BASE: bddarigo-auth
      JWT_SECRET: ${JWT_SECRET}
      JWT_EXPIRES_IN: ${JWT_EXPIRES_IN:-7d}
    depends_on:
      mongodb:
        condition: service_healthy
    networks:
      - darigo-frontend
      - darigo-backend
    restart: on-failure

  cuisine-service:
    build:
      context: .
      dockerfile: cuisine-service/Dockerfile
    container_name: darigo-cuisine
    environment:
      PORT: 3002
      MONGO_URI: mongodb://${MONGO_USER:-admin}:${MONGO_PASSWORD:-secret}@mongodb:27017
      NOM_BASE: bddarigo-cuisines
      AUTH_SERVICE_URL: http://auth-service:3001
    depends_on:
      mongodb:
        condition: service_healthy
      auth-service:
        condition: service_started
    networks:
      - darigo-frontend
      - darigo-backend
    restart: on-failure

  commande-service:
    build:
      context: .
      dockerfile: commande-service/Dockerfile
    container_name: darigo-commande
    environment:
      PORT: 3003
      MONGO_URI: mongodb://${MONGO_USER:-admin}:${MONGO_PASSWORD:-secret}@mongodb:27017
      NOM_BASE: bddarigo-commandes
      AUTH_SERVICE_URL: http://auth-service:3001
      CUISINE_SERVICE_URL: http://cuisine-service:3002
      RABBITMQ_URL: amqp://${RABBITMQ_USER:-guest}:${RABBITMQ_PASS:-guest}@rabbitmq:5672
      EXCHANGE_NOM: darigo
    depends_on:
      mongodb:
        condition: service_healthy
      rabbitmq:
        condition: service_healthy
      auth-service:
        condition: service_started
      cuisine-service:
        condition: service_started
    networks:
      - darigo-frontend
      - darigo-backend
    restart: on-failure

  livraison-service:
    build:
      context: .
      dockerfile: livraison-service/Dockerfile
    container_name: darigo-livraison
    environment:
      PORT: 3004
      MONGO_URI: mongodb://${MONGO_USER:-admin}:${MONGO_PASSWORD:-secret}@mongodb:27017
      NOM_BASE: bddarigo-livraisons
      AUTH_SERVICE_URL: http://auth-service:3001
      RABBITMQ_URL: amqp://${RABBITMQ_USER:-guest}:${RABBITMQ_PASS:-guest}@rabbitmq:5672
      EXCHANGE_NOM: darigo
    depends_on:
      mongodb:
        condition: service_healthy
      rabbitmq:
        condition: service_healthy
    networks:
      - darigo-frontend
      - darigo-backend
    restart: on-failure

  notif-service:
    build:
      context: .
      dockerfile: notif-service/Dockerfile
    container_name: darigo-notif
    environment:
      PORT: 3005
      MONGO_URI: mongodb://${MONGO_USER:-admin}:${MONGO_PASSWORD:-secret}@mongodb:27017
      NOM_BASE: bddarigo-notifs
      AUTH_SERVICE_URL: http://auth-service:3001
      RABBITMQ_URL: amqp://${RABBITMQ_USER:-guest}:${RABBITMQ_PASS:-guest}@rabbitmq:5672
      EXCHANGE_NOM: darigo
    depends_on:
      rabbitmq:
        condition: service_healthy
    networks:
      - darigo-backend    # Pas de frontend — pas de port HTTP entrant depuis Nginx
    restart: on-failure

  nginx:
    image: nginx:1.25-alpine
    container_name: darigo-nginx
    ports:
      - "80:80"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro    # :ro = lecture seule
    depends_on:
      - auth-service
      - cuisine-service
      - commande-service
      - livraison-service
    networks:
      - darigo-frontend   # Nginx uniquement en frontend — ne voit pas MongoDB
    restart: on-failure

volumes:
  mongo_data:
  rabbitmq_data:

networks:
  darigo-frontend:
    driver: bridge   # Nginx + services Node.js
  darigo-backend:
    driver: bridge   # Services Node.js + MongoDB + RabbitMQ

3
Demarrage et 20 tests de validation
bashDemarrage complet
cd darigo
docker-compose up --build -d   # Build + demarrage

docker-compose ps              # 7 services Running
docker-compose logs -f         # Suivre tous les logs
#TestAttendu
T1docker-compose ps7 services Up
T2GET http://localhost/nginx-health200 { statut: actif }
T3GET http://localhost/auth/health200 via Nginx → auth-service
T4GET http://localhost/cuisines/200 liste restaurants
T5POST /auth/register (role client) → token_client201
T6POST /auth/register (role restaurateur) → token_resto201
T7POST /auth/register (role livreur) → token_livreur201
T8POST /auth/login → tokens pour chaque role200 + JWT
T9POST /cuisines/ avec token_resto → restaurantId201
T10POST /cuisines/:id/plats (3 plats) avec token_resto201 × 3
T11GET /cuisines/:id/plats → 3 plats avec disponible:true200
T12POST /commandes avec token_client → commandeId201 + total
T13Logs notif-service : SMS envoyeMessage consomme immediatement
T14Logs livraison-service : Mission creeeMission creee depuis RabbitMQ
T15GET /livraisons avec token_livreur → mission disponible200 + liste
T16PATCH /livraisons/:id/accepter avec token_livreur200 Mission acceptee
T17PATCH /livraisons/:id/statut { statut: en_route } token_livreur200
T18PATCH /livraisons/:id/statut { statut: livree }200
T19docker-compose exec nginx ping mongodbECHEC — isolation reseau validee
T20Arreter notif-service → 3 commandes → relancer → 3 messagesResilience RabbitMQ confirmee
Projet Final — Etape 8 Docker Hub · App Service · Azure
⏱ 22 min

Etape 8 — Deploiement Azure App Service

Build + tag + push des 5 images (contexte racine pour shared/), docker-compose.azure.yml (differences vs Compose local), deploiement multi-conteneurs az webapp, App Settings pour les secrets, validation flux complet en production, nettoyage des ressources.

✓ Ce que vous allez faire
  • Construire, taguer et pousser les 5 images sur Docker Hub
  • Creer docker-compose.azure.yml — adapter pour Azure App Service
  • Deployer en multi-conteneurs sur Azure App Service Plan B1
  • Injecter les secrets via App Settings (jamais dans les images)
  • Valider le deploiement : flux complet register → commande en production

1
Publier les 5 images sur Docker Hub
bashBuild, tag et push des 5 images
docker login  # Se connecter a Docker Hub

USERNAME=VOTRE_USERNAME  # Remplacer par votre username Docker Hub

# Construire depuis la racine du projet (contexte = . pour inclure shared/)
for service in auth-service cuisine-service commande-service livraison-service notif-service; do
  echo "=== Build $service ==="
  docker build     -f $service/Dockerfile     -t darigo-${service%%-service}:1.0.0 \    # darigo-auth:1.0.0
    .                                          # Contexte = racine

  docker tag darigo-${service%%-service}:1.0.0 $USERNAME/darigo-${service%%-service}:1.0.0
  docker tag darigo-${service%%-service}:1.0.0 $USERNAME/darigo-${service%%-service}:latest
  docker push $USERNAME/darigo-${service%%-service}:1.0.0
  docker push $USERNAME/darigo-${service%%-service}:latest
done

# Verifier les 5 depots sur Docker Hub
# hub.docker.com/u/VOTRE_USERNAME

2
docker-compose.azure.yml — version App Service
yamldarigo/docker-compose.azure.yml
version: '3.9'
# DIFFERENCES avec le Compose local :
# - Pas de build: (images pre-construites sur Docker Hub)
# - Pas de volumes bind mount
# - Secrets injectes via App Settings Azure (${VAR})
# - DNS interne : noms de services = hostnames Azure

services:

  auth-service:
    image: VOTRE_USERNAME/darigo-auth:1.0.0
    environment:
      WEBSITES_PORT: 3001
      PORT: 3001
      NODE_ENV: production
      MONGO_URI: ${MONGO_URI}           # Injecte depuis App Settings
      NOM_BASE: bddarigo-auth
      JWT_SECRET: ${JWT_SECRET}
      JWT_EXPIRES_IN: 7d
    ports:
      - '3001:3001'

  cuisine-service:
    image: VOTRE_USERNAME/darigo-cuisine:1.0.0
    environment:
      PORT: 3002
      NODE_ENV: production
      MONGO_URI: ${MONGO_URI}
      NOM_BASE: bddarigo-cuisines
      AUTH_SERVICE_URL: http://auth-service:3001   # DNS interne Azure
    ports:
      - '3002:3002'

  commande-service:
    image: VOTRE_USERNAME/darigo-commande:1.0.0
    environment:
      PORT: 3003
      NODE_ENV: production
      MONGO_URI: ${MONGO_URI}
      NOM_BASE: bddarigo-commandes
      AUTH_SERVICE_URL: http://auth-service:3001
      CUISINE_SERVICE_URL: http://cuisine-service:3002
      RABBITMQ_URL: ${RABBITMQ_URL}
      EXCHANGE_NOM: darigo
    ports:
      - '3003:3003'

  livraison-service:
    image: VOTRE_USERNAME/darigo-livraison:1.0.0
    environment:
      PORT: 3004
      NODE_ENV: production
      MONGO_URI: ${MONGO_URI}
      NOM_BASE: bddarigo-livraisons
      AUTH_SERVICE_URL: http://auth-service:3001
      RABBITMQ_URL: ${RABBITMQ_URL}
      EXCHANGE_NOM: darigo
    ports:
      - '3004:3004'

  notif-service:
    image: VOTRE_USERNAME/darigo-notif:1.0.0
    environment:
      PORT: 3005
      NODE_ENV: production
      MONGO_URI: ${MONGO_URI}
      NOM_BASE: bddarigo-notifs
      AUTH_SERVICE_URL: http://auth-service:3001
      RABBITMQ_URL: ${RABBITMQ_URL}
      EXCHANGE_NOM: darigo
      # Pas de ports: — consumer pur, pas accessible depuis Nginx

3
Deploiement Azure CLI — etapes completes
bashDeploiement complet — 5 commandes
az login

# 1 — Groupe de ressources
az group create --name rg-darigo-prod --location francecentral

# 2 — App Service Plan B1 Linux
az appservice plan create   --name plan-darigo   --resource-group rg-darigo-prod   --sku B1   --is-linux

# 3 — Web App multi-conteneurs
az webapp create   --name darigo-app \                     # Nom unique mondialement
  --resource-group rg-darigo-prod   --plan plan-darigo   --multicontainer-config-type compose   --multicontainer-config-file docker-compose.azure.yml

# 4 — Injecter les secrets (jamais dans l'image ni dans le YAML)
az webapp config appsettings set   --name darigo-app   --resource-group rg-darigo-prod   --settings     MONGO_URI='mongodb+srv://admin:pass@cluster.mongodb.net'     JWT_SECRET=darigo_prod_jwt_secret_2024_xyz     RABBITMQ_URL='amqps://user:pass@rabbitmq.cloudamqp.com/vhost'

# 5 — Activer les logs
az webapp log config   --name darigo-app   --resource-group rg-darigo-prod   --application-logging filesystem   --docker-container-logging filesystem

# Streamer les logs
az webapp log tail --name darigo-app --resource-group rg-darigo-prod

# URL publique
az webapp show --name darigo-app --resource-group rg-darigo-prod   --query defaultHostName --output tsv
# → darigo-app.azurewebsites.net

4
Validation finale — flux complet en production
#Requete sur https://darigo-app.azurewebsites.netAttendu
T1GET /auth/health200 { service: auth-service }
T2POST /auth/register (client)201 + userId
T3POST /auth/register (restaurateur)201
T4POST /auth/login client → token_client200 + JWT
T5POST /auth/login restaurateur → token_resto200 + JWT
T6POST /cuisines/ avec token_resto201 + restaurantId
T7POST /cuisines/:id/plats (couscous, tajine, pastilla)201 × 3
T8GET /cuisines/ → restaurant visible200
T9POST /commandes avec token_client + platId + quantite201 + total
T10GET /commandes avec token_client200 + historique
T11az webapp log tail → logs des 5 services visiblesTous actifs
T12Portail → Metriques → CPU + HTTP 2xx apres testsGraphiques avec pics
T13az webapp restart darigo-app → GET /auth/healthRecovery automatique
T14az group delete --name rg-darigo-prod --yes --no-waitNettoyage final
✓ Parcours complet du module — realise
  • Partie 1 : Cloud Native, 12 facteurs, CNCF
  • Partie 2 : API REST Node.js + MongoDB + JWT + bcrypt + helmet
  • Partie 3 : Microservices, axios inter-services, RabbitMQ, Nginx
  • Partie 4 : Docker, Dockerfile, Docker Compose, reseaux, volumes, healthchecks
  • Partie 5 : Azure App Service, docker tag/push, multi-conteneurs, Log Stream
  • Projet Final : dariGo — plateforme complete Cloud Native deployee sur Azure
Dans docker-compose.azure.yml, notif-service n'a pas de ports: mais les autres services ont WEBSITES_PORT. Pourquoi cette difference ?