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).
- 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.
- 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 ?
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éristique | Explication | Exemple concret |
|---|---|---|
| Libre-service à la demande | Provisionner des ressources automatiquement, sans intervention humaine du fournisseur | Créer un serveur sur Azure en 2 minutes depuis son navigateur |
| Accès réseau large | Ressources accessibles depuis n'importe quel appareil standard | Accéder à son application depuis un téléphone à Fès ou un PC à Rabat |
| Mise en commun | Ressources physiques partagées entre plusieurs clients de façon transparente | 10 entreprises marocaines partagent le même datacenter Microsoft à Amsterdam |
| Élasticité rapide | Ressources augmentées ou réduites rapidement selon les besoins | Avito.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.
| Fournisseur | Nom complet | Parts de marché 2024 | Points forts |
|---|---|---|---|
| AWS | Amazon Web Services | ~32% | Le plus ancien, 200+ services, présence mondiale maximale |
| Azure | Microsoft Azure | ~23% | Intégration parfaite avec l'écosystème Microsoft (Office 365, Active Directory) |
| GCP | Google Cloud Platform | ~12% | Excellence en Big Data, Machine Learning et Kubernetes (inventé par Google) |
| OVHcloud | OVH | ~2% | Fournisseur européen, populaire au Maghreb pour sa conformité RGPD |
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.
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ère | Cloud public | Cloud privé | Cloud hybride |
|---|---|---|---|
| Coût initial | Très faible | Très élevé | Modéré |
| Sécurité des données | Partagée | Maximale | Adaptée par couche |
| Scalabilité | Illimitée | Limitée au matériel | Flexible |
| Contrôle | Faible | Total | Partiel |
| Exemple marocain | Avito.ma (AWS) | Bank Al-Maghrib | Maroc Telecom |
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).
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.
| Couche | On-Premise | IaaS | PaaS | SaaS |
|---|---|---|---|---|
| Application | Client | Client | Client | Fournisseur |
| Données | Client | Client | Client | Fournisseur |
| Runtime (Node.js...) | Client | Client | Fournisseur | Fournisseur |
| Système d'exploitation | Client | Client | Fournisseur | Fournisseur |
| Serveurs physiques | Client | Fournisseur | Fournisseur | Fournisseur |
| Exemple Azure | Datacenter propre | Virtual Machines | App Service | Microsoft 365 |
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.
- 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
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ème | Ce qui se passe concrètement | Impact |
|---|---|---|
| Déploiement tout-ou-rien | Corriger un bug dans le module SMS oblige à redéployer TOUTE l'application — 45 min d'arrêt complet | 3 millions d'utilisateurs sans service à chaque mise à jour |
| Scalabilité rigide | Le module Facturation est très sollicité en fin de mois. Impossible de scaler que lui : on duplique TOUT le monolithe | Coûts énormes pour scaler des modules inutilement |
| Effet domino des bugs | Un bug dans le module SMS provoque une erreur mémoire qui fait planter toute l'application | Un problème mineur devient une panne totale |
| Technologies figées | Le monolithe est en Java 8. Impossible d'utiliser Node.js pour le module temps réel sans tout réécrire | Innovation technologique bloquée |
| Collaboration difficile | 20 développeurs sur le même code — conflits de merge permanents | Productivité réduite, risque d'erreurs élevé |
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.
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 technique | Traduction simple |
|---|---|
| Scalable | Capable 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ésilient | Capable de se remettre automatiquement d'une panne — si le paiement est surchargé, l'application continue avec un message d'attente |
| Observable | On peut voir en temps réel ce qui se passe — tableau de bord avec requêtes, erreurs, temps de réponse par service |
| Immuable | L'infrastructure ne se modifie pas, elle se remplace — on déploie une nouvelle version et on supprime l'ancienne |
| Pilier | Définition courte | Dans ce module |
|---|---|---|
| 1. Microservices | Découper l'application en petits services autonomes, chacun responsable d'une seule fonction | Partie 3 — créer une application microservices Node.js (auth, produit, commande) |
| 2. Conteneurs | Emballer chaque service avec tout ce dont il a besoin, de manière portable et isolée | Partie 4 — Docker : images, conteneurs, Docker Compose |
| 3. CI/CD | Automatiser 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. DevOps | Culture et pratiques qui rapprochent développement et opérations pour livrer plus vite | Inté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.
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.
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.
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ère | Machine Virtuelle (VM) | Conteneur Docker |
|---|---|---|
| Taille | Plusieurs Go (OS complet inclus) | Quelques Mo à centaines de Mo |
| Démarrage | 2 à 5 minutes | Moins d'une seconde |
| Isolation | Complète (OS dédié) | Processus isolé, OS partagé |
| Portabilité | Lourde (fichier .vmdk, .vhd) | Légère (image sur Docker Hub) |
| Densité serveur | 5 à 20 VMs par serveur physique | Centaines de conteneurs par serveur |
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.
| # | Facteur | Principe | Application dans ce module |
|---|---|---|---|
| I | Codebase | Un seul dépôt Git par service | Chaque microservice a son propre repo GitHub |
| II | Dependencies | Déclarer explicitement toutes les dépendances | package.json liste exactement les packages requis |
| III | Config | Configuration dans les variables d'environnement, jamais dans le code | Secrets JWT et URLs MongoDB dans .env, jamais en dur dans index.js |
| VI | Processes | Application sans état (stateless) | Toutes les APIs REST de ce module sont stateless — chaque requête est indépendante |
| VII | Port Binding | L'application expose son propre port | app.listen(3000) — Express écoute sur le port 3000 |
| X | Dev/Prod Parity | Environnements de développement et production similaires | Docker garantit que ce qui tourne sur votre PC tourne identiquement sur Azure |
| XI | Logs | Écrire les logs sur stdout | console.log() Node.js → Azure collecte automatiquement les logs |
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.
| Critère | Application Traditionnelle | Application Cloud Native |
|---|---|---|
| Architecture | Un seul bloc indivisible | Ensemble de microservices indépendants |
| Déploiement | Tout ou rien, 30–60 min d'arrêt | Service par service, zéro interruption |
| Scalabilité | L'application entière, coût élevé | Service par service, coût optimisé |
| Panne | Un bug peut tout planter | Isolation : une panne reste locale |
| Technologie | Une seule techno pour tout | Liberté par service (polyglotte) |
| Infrastructure | Serveurs fixes, configurés manuellement | Conteneurs, infrastructure immuable |
| Temps de livraison | Semaines ou mois | Heures ou jours |
| Exemple marocain | SI CNSS avant transformation | Plateforme Damancom après transformation |
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.
Pour chaque description, déterminez si l'application est Cloud Native ou non, et pourquoi.
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.
- 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 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()etmodule.exports - Créer un premier serveur HTTP natif
- Pourquoi Express simplifie radicalement le développement d'APIs
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ère | Node.js | PHP | Java Spring |
|---|---|---|---|
| Langage | JavaScript (front + back identique) | PHP (dédié back) | Java (verbeux) |
| Démarrage conteneur | < 1 seconde | 1–2 secondes | 5–30 secondes |
| Consommation mémoire | Très faible (~50 Mo) | Modérée | Élevée (JVM ~200 Mo+) |
| Scalabilité Cloud Native | Idéal : léger, stateless, rapide | Acceptable | Très bien mais coûteux |
| Packages disponibles | 1,5 million+ via npm | ~100k via Composer | ~500k via Maven |
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.
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.
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.
// 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
// 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.
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.
# 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
{
"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
}
}
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.
require() et module.exportsUn 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
.jsdu 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')
// 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
};
// 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
Si Node.js affiche Error: Cannot find module './calculatrice' :
- Le fichier
calculatrice.jsest-il dans le même dossier ? Vérifier avecls(Mac/Linux) oudir(Windows). - Le nom du fichier est-il exactement identique ? Node.js est sensible à la casse sur Linux.
- Le chemin commence-t-il par
./? Écrirerequire('./calculatrice')et nonrequire('calculatrice').
| Module natif | Utilité | Exemple d'utilisation |
|---|---|---|
http | Créer des serveurs HTTP sans Express | const http = require('http') |
fs | Lire et écrire des fichiers | const fs = require('fs') |
path | Construire des chemins de fichiers cross-platform | const path = require('path') |
events | Créer et gérer des événements | const EventEmitter = require('events') |
os | Infos sur le système d'exploitation | const os = require('os') |
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.
// 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 à tester | Résultat attendu | Code HTTP |
|---|---|---|
http://localhost:3000/ | { message: 'API Node.js opérationnelle' } | 200 OK |
http://localhost:3000/produits | Tableau des 2 produits | 200 OK |
http://localhost:3000/clients | { erreur: 'Route introuvable' } | 404 Not Found |
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/elsepar route → illisible dès 10 routes. - Paramètres d'URL : extraire l'
iddans/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.
// 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 natif | Express.js |
|---|---|---|
| Route GET | if (url === '/x' && method === 'GET') | app.get('/x', handler) |
Paramètre :id | req.url.split('/')[2] — fragile | req.params.id — sûr et simple |
| Lire le body JSON | ~15 lignes de code manuel | app.use(express.json()) — 1 ligne |
| Envoyer du JSON | res.writeHead(200,{...}); res.end(JSON.stringify(...)) | res.json({...}) |
| Middleware global | Modification de chaque route | app.use(monMiddleware) |
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.
Lisez le code ci-dessous. Identifiez les 5 erreurs avant de regarder le corrigé.
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é');
});
- 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()—expressest 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 dansreq.params.req.bodycontient 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 estlistenen minuscules.
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.
- 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
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.
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
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.
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);
});
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.
mkdir mon-api && cd mon-api
npm init -y
npm install express
npm install nodemon --save-dev
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}`);
});
app.METHODE(CHEMIN, HANDLER)
METHODE → get / post / put / delete (toujours en minuscules)
CHEMIN → '/produits', '/produits/:id'
HANDLER → function(req, res) { ... }
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}`));
| Méthode Express | Utilisation | Équivalent Node.js natif |
|---|---|---|
res.json(objet) | Envoyer du JSON — définit Content-Type: application/json automatiquement | res.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 HTML | res.end(texte) |
res.sendStatus(204) | Envoyer uniquement un code HTTP, sans body | res.writeHead(204); res.end() |
| Code | Signification | Quand l'utiliser |
|---|---|---|
| 200 OK | Succès | GET réussi, PUT réussi, DELETE réussi |
| 201 Created | Ressource créée | POST réussi — création d'un produit, utilisateur... |
| 204 No Content | Succès sans réponse | DELETE réussi quand on ne renvoie rien |
| 400 Bad Request | Données invalides | Body JSON mal formé, champ obligatoire absent |
| 401 Unauthorized | Auth requise | Token JWT absent ou invalide |
| 403 Forbidden | Accès refusé | Authentifié mais sans les droits suffisants |
| 404 Not Found | Ressource introuvable | ID inexistant en base |
| 500 Server Error | Erreur serveur | Exception non gérée dans le code |
Pour chaque requête ci-dessous, déterminez le code HTTP que le serveur doit renvoyer et pourquoi.
| Requête | Situation | Code attendu |
|---|---|---|
GET /produits | Tableau de 3 produits retourné avec succès | 200 OK |
GET /produits/99 | Aucun produit avec l'id 99 dans les données | 404 Not Found |
POST /produits avec { nom: 'Tajine' } | Le champ prix est obligatoire mais absent | 400 Bad Request |
POST /produits avec { nom: 'Tajine', prix: 120 } | Produit créé avec succès | 201 Created |
DELETE /produits/1 | Produit supprimé avec succès | 200 OK ou 204 |
GET /commandes | Route non définie dans le serveur | 404 Not Found |
app.get('/produits/:id', ...), quelle ligne de code extrait correctement l'id numérique depuis l'URL /produits/5 ?app.use(express.json()) avant de définir les routes POST ?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.
- 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 avecfs.readFile - Implémenter
GET /equipes/:id— une équipe par identifiant - Installer Postman et tester les 4 cas (200, 404, 400, 500)
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ègle | Correct | 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" } |
{
"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
}
]
}
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.
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éthode | URL | Action | Code succès |
|---|---|---|---|
GET | /equipes | Retourner toutes les équipes | 200 OK |
GET | /equipes/:id | Retourner une équipe par id | 200 OK |
POST | /equipes | Créer une nouvelle équipe | 201 Created |
PUT | /equipes/:id | Modifier une équipe | 200 OK |
DELETE | /equipes/:id | Supprimer une équipe | 200 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
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.
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
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;
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}`));
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.
| URL | Résultat attendu | Code HTTP | Cas testé |
|---|---|---|---|
GET /equipes | Tableau des 5 équipes + total: 5 | 200 | Succès nominal |
GET /equipes/3 | Objet équipe FUS Rabat | 200 | Id valide existant |
GET /equipes/99 | { erreur: 'Equipe introuvable', id_demande: 99 } | 404 | Id valide mais inexistant |
GET /equipes/abc | { erreur: 'L'id doit être un nombre entier' } | 400 | Id 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é | Navigateur | Postman |
|---|---|---|
| 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 |
Remplissez les blancs pour que la route GET /stades/:nom fonctionne correctement :
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)
- (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.nompour les paramètres d'URL - (10)
nomDemande— la variable contenantreq.params.nom - (11)
404— ressource introuvable - (12)
status—res.status(200).json() - (13)
router— exporter le router
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).
- Définition et rôle d'un middleware Express — chaînage et
next() - Pourquoi
req.bodyestundefinedsansexpress.json() - Implémenter
POST /equipes— validation +fs.writeFile - Implémenter
PUT /equipes/:id— fusion partielle avecObject.assign - Tester POST et PUT dans Postman avec des corps JSON
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.
// 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 });
});
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.
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.
// 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
});
});
});
});
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.
// ── 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.assign | Sans (écrasement direct) |
|---|---|---|
{ titres_nationaux: 23 } | Seul titres_nationaux change — nom, ville, fondation intacts | L'équipe n'aurait plus que titres_nationaux — tous les autres champs supprimés |
{ stade: "Nouveau stade" } | Seul stade change | Tous les autres champs supprimés |
app.use(express.json()), que contient req.body dans une route POST ?Math.max(...equipes.map(e => e.id)) + 1 pour générer un nouvel id ?.find() et .findIndex() sur un tableau ?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.
- Implémenter
DELETE /equipes/:idavecfilter() - Différence entre
splice()etfilter()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
// ── 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éthode | Comportement | Recommandation |
|---|---|---|
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 |
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(...)).
// 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
// ...
});
});
| Code | Signification | Exemple de déclenchement |
|---|---|---|
| 400 | Bad Request — données invalides | id non numérique, champ obligatoire absent, valeur hors plage |
| 401 | Unauthorized — non authentifié | Token JWT absent dans les headers |
| 403 | Forbidden — authentifié mais sans droits | Utilisateur lambda tente de supprimer un enregistrement admin |
| 404 | Not Found — ressource introuvable | GET /equipes/99 — aucune équipe avec cet id |
| 409 | Conflict — conflit de données | POST /equipes avec un nom déjà existant |
| 500 | Internal Server Error — bug serveur | Erreur de lecture/écriture fichier, exception non gérée |
| Route | Action | Succès | Erreurs possibles |
|---|---|---|---|
GET /equipes | Lire toutes les équipes | 200 + tableau | 500 (lecture fichier) |
GET /equipes/:id | Lire une équipe | 200 + équipe | 400 (id non numérique), 404 (id inexistant), 500 |
POST /equipes | Créer une équipe | 201 + nouvelle équipe | 400 (validation échoue), 500 (écriture) |
PUT /equipes/:id | Modifier une équipe | 200 + équipe modifiée | 400 (id), 404 (inexistant), 500 |
DELETE /equipes/:id | Supprimer une équipe | 200 + équipe supprimée | 400 (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.jsonavec au moins 5 joueurs marocains (id, nom, poste, equipe_id, nationalite) - Créer
routes/joueurs.jsavec 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
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.
- MongoDB vs SQL — vocabulaire, structure et différences fondamentales
- Installer MongoDB et MongoDB Compass
- Créer une base
bdmonapiet une collectionequipe - Se connecter à MongoDB depuis Node.js avec le package
mongodb - Lire des documents MongoDB depuis une route Express
- 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 MongoDB | Différence clé |
|---|---|---|
| Base de données | Base de données | Même concept — un conteneur de données nommé |
| Table | Collection | Une collection stocke des documents JSON — pas des lignes uniformes |
| Ligne (row) | Document | Un 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 $lookup | MongoDB favorise les documents imbriqués plutôt que les jointures |
- 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).
# 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
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 };
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}`);
});
});
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);
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.
| Opération SQL | Équivalent MongoDB (Node.js) |
|---|---|
SELECT * FROM equipe | collection.find({}).toArray() |
SELECT * FROM equipe WHERE id = 3 | collection.findOne({ _id: objectId }) |
INSERT INTO equipe VALUES (...) | collection.insertOne({ nom: '...', ville: '...' }) |
UPDATE equipe SET titres = 23 WHERE id = 1 | collection.updateOne({ _id: id }, { $set: { titres_nationaux: 23 } }) |
DELETE FROM equipe WHERE id = 3 | collection.deleteOne({ _id: objectId }) |
SELECT * FROM equipe WHERE ville = 'Rabat' | collection.find({ ville: 'Rabat' }).toArray() |
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.
ObjectId— comprendre et convertir les identifiants MongoDBGET /equipesavecfind(), projection et triGET /equipes/:idavecfindOne()par ObjectIdPOST /equipesavecinsertOne()et validation- Tester dans Postman et vérifier dans MongoDB Compass
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.
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' });
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).
// ── 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' });
}
});
// ── 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' });
}
});
// ── 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 retour | Type | Valeur |
|---|---|---|
acknowledged | boolean | true si MongoDB a confirmé l'insertion |
insertedId | ObjectId | L'_id généré automatiquement pour le nouveau document |
collection.findOne({ _id: objectId }) si aucun document ne correspond ?ObjectId.isValid(id) avant new ObjectId(id) ?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.
PUT /equipes/:idavecupdateOne()et l'opérateur$setDELETE /equipes/:idavecdeleteOne()etdeletedCount- Les opérateurs MongoDB :
$set,$inc,$push,$pull - Bilan comparatif API JSON vs API MongoDB
- TP consolidation CRUD complet sur la ressource Stades
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.
// ── 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érateur | Effet | Exemple |
|---|---|---|
$set | Modifier des champs spécifiques | { $set: { titres_nationaux: 23 } } |
$unset | Supprimer un champ du document | { $unset: { stade: '' } } |
$inc | Incrémenter un champ numérique | { $inc: { titres_nationaux: 1 } } |
$push | Ajouter un élément à un tableau | { $push: { joueurs: 'Ilias Akhomach' } } |
$pull | Retirer un élément d'un tableau | { $pull: { joueurs: 'Ilias Akhomach' } } |
// ── 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' });
}
});
| Critère | API JSON (fichiers) | API MongoDB |
|---|---|---|
| Installation | Aucune — fichiers déjà là | MongoDB à installer + configurer |
| Concurrence | Dangereuse — corruption possible | Gérée nativement par MongoDB |
| Performance | Lit tout le fichier à chaque requête | Index, requêtes ciblées, scalable |
| Identifiants | Entiers auto-incrémentés manuels | ObjectId générés automatiquement |
| Requêtes avancées | Filtre manuel en JS (.filter()) | Opérateurs MongoDB ($gte, $in, $regex...) |
| Production | Non — données perdues si crash | Oui — persistance garantie |
| Cas d'usage | Prototypage rapide, séances 5-7 | Production, séances 8-14 |
updateOne() si le filtre ne correspond à aucun document ?$set et un remplacement direct dans updateOne() ?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).
- 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 MongoDBPOST /auth/login: vérification mot de passe et génération du token
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 :
| Critere | Sessions (cookie + serveur) | JWT (token) |
|---|---|---|
| Stockage | Cote serveur en memoire ou BDD | Cote client uniquement |
| Scalabilite | Problematique avec load balancer | Parfaite — chaque serveur verifie le token seul |
| Compatibilite | Navigateur uniquement (cookie) | Mobile, SPA, APIs tierces |
| Revocation | Immediate (supprimer la session) | Complexe — token valide jusqu'a expiration |
| Architecture REST | Viole le principe stateless | Respecte le principe stateless |
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.
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.
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.
npm install jsonwebtoken bcryptjs dotenv
MONGO_URI=mongodb://localhost:27017
JWT_SECRET=une_chaine_aleatoire_longue_et_complexe_min_32_chars
JWT_EXPIRES_IN=7d
PORT=3000
require('dotenv').config(); // TOUJOURS en premiere ligne
const express = require('express');
// ...
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;
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.
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.
- Middleware
isAuthenticated: extraire et verifier le token avecjwt.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
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;
| Cas | Erreur jwt.verify() | Code HTTP |
|---|---|---|
| Token absent | — | 401 |
| Format non Bearer | — | 401 |
| Signature incorrecte | JsonWebTokenError | 401 |
| Token expire | TokenExpiredError | 401 |
| Token valide, role insuffisant | — | 403 |
// 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;
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
| Etape | Requete Postman | Body | Resultat attendu |
|---|---|---|---|
| 1 | POST /auth/register | {"email":"test@ma.ma","motDePasse":"Secret123"} | 201 + userId |
| 2 | POST /auth/login | {"email":"test@ma.ma","motDePasse":"Secret123"} | 200 + token |
| 3 | DELETE /equipes/:id | — | 401 (pas de token) |
| 4 | DELETE /equipes/:id + Authorization: Bearer <token> | — | 403 si role=user, 200 si role=admin |
TokenExpiredError. Que s'est-il passe ?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.
- 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
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.
npm install helmet cors express-rate-limit express-validator
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);
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...
});
| Attaque | Protection |
|---|---|
| Brute force login | Rate limiting strict sur /auth/login |
| Enumeration d'emails | Meme message d'erreur pour email inexistant et mot de passe faux |
| Injection NoSQL | express-validator sanitise les entrees avant qu'elles atteignent MongoDB |
| XSS via API | helmet + Content-Security-Policy header |
| CSRF | CORS strict limite les origines autorisees |
| Exposition infra | helmet supprime X-Powered-By et les headers revelateurs |
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
/auth/login (10 req/15 min) que sur les autres routes (100 req/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.
- 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
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.
| Service | Responsabilite unique | Port | Base de donnees |
|---|---|---|---|
auth-service | Register, login, verification JWT | 3001 | bdauth — collection utilisateur |
equipe-service | CRUD complet sur les equipes | 3002 | bdequipes — collection equipe |
stade-service | CRUD complet sur les stades | 3003 | bdstades — collection stade |
competition-service | Gestion des competitions et calendriers | 3004 | bdcompetitions — collection competition |
api-gateway | Point d'entree unique, routing, auth | 3000 | Aucune — proxy uniquement |
- 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.
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.
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'));
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
bdequipesn'affecte pasbdauth. - Choix technologique libre : equipe-service peut utiliser MongoDB, competition-service peut utiliser PostgreSQL.
- Isolation des pannes : si
bdstadesest 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.
| Pattern | Probleme resolu | Principe |
|---|---|---|
| Circuit Breaker | Service en panne bloque les appelants | Detecter les echecs et "ouvrir le circuit" — echouer rapidement plutot qu'attendre |
| Saga | Coherence des donnees sans transaction distribuee | Etapes avec compensations — si etape 3 echoue, annuler les etapes 1 et 2 |
| API Gateway | Client expose a N services differents | Point d'entree unique — routing, auth et rate limiting centralises |
| Database per Service | Services couples via base partagee | Chaque service possede sa propre base — couplage uniquement via API |
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.
| Indicateur | Monolithe | Microservices |
|---|---|---|
| Taille de l'equipe | < 5 developpeurs | > 5 developpeurs, equipes separees |
| Stade du projet | Phase initiale, besoins flous | Produit etabli, besoins stables |
| Scalabilite | Uniforme — toute l'app ensemble | Ciblee — scaler uniquement le service charge |
| Disponibilite requise | Tolerante aux interruptions de deploi | Zero downtime obligatoire |
| Exemple marocain adapte | API d'une startup en phase MVP | Plateforme Damancom, Inwi (infrastructure etablie) |
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.
- 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
| Critere | API REST classique | Microservice |
|---|---|---|
| Perimetre | Peut couvrir plusieurs domaines | Un seul domaine metier (Single Responsibility) |
| Deploiement | Deploye avec le reste de l'application | Deploye, stoppe et scale independamment |
| Base de donnees | Partage souvent la BDD de l'application | Possede sa propre BDD — non partagee |
| Cycle de vie | Dependant de l'application globale | Equipe et pipeline de deploiement dedies |
| Scalabilite | Scalabilite de toute l'application | Scale uniquement le service charge |
| Defaillance | Un bug peut impacter tout le systeme | La defaillance reste isolee au service |
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.
| Type | Technologie | Cas d'usage | Avantage | Inconvenient |
|---|---|---|---|---|
| Synchrone | HTTP / axios | Verifier un token, lire un produit | Simple, reponse immediate | Couplage temporal — si le service appele est hors ligne, l'appelant echoue |
| Asynchrone | RabbitMQ, Kafka | Email de confirmation, decrement stock | Decouplage total — resilient aux pannes | Complexite, pas de reponse immediate |
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
// 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' });
}
});
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.
- 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
| Caracteristique | Node.js | Java Spring |
|---|---|---|
| Temps de demarrage | ~100 ms | ~10-30 secondes |
| Memoire au repos | ~30 Mo | ~300 Mo (JVM) |
| I/O concurrent | Event loop — milliers de requetes | Thread pool — limite en nombre |
| Densite sur serveur | Dizaines de services par machine | Quelques services par machine |
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.
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;
skip utiliser ?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.
- 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
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.
// 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' });
}
});
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 axios | Quand presente | Exemple |
|---|---|---|
err.response | Le serveur a repondu avec un code d'erreur | auth-service retourne 401 — token invalide |
err.code | La requete n'a pas atteint le serveur | ECONNREFUSED (service arrete), ECONNABORTED (timeout) |
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 });
});
nomProduit et prixUnitaire dans la commande au lieu de les lire depuis produit-service a chaque consultation ?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.
- 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)
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.
| Concept | Role | Analogie |
|---|---|---|
| Producer | Publie des messages vers l'exchange | L'expediteur d'un colis |
| Exchange | Recoit les messages et les route vers les bonnes queues selon les regles | Le centre de tri postal |
| Queue | File d'attente FIFO — stocke les messages jusqu'au traitement | La boite aux lettres du destinataire |
| Consumer | S'abonne a une queue et traite les messages | Le destinataire qui vide sa boite |
| Acknowledgement | Confirmation que le message a ete traite — RabbitMQ le supprime de la queue | Signer le bon de reception |
| Broker | Le serveur RabbitMQ qui gere exchanges et queues | La poste entiere |
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.
| Type | Routage | Cas d'usage microservices |
|---|---|---|
| Direct | Cle de routage exacte — un message vers une queue precise | Decrement stock apres commande — seul stock-service recoit |
| Fanout | Broadcast — toutes les queues liees recoivent le message | Notifier tous les services d'un evenement systeme (maintenance) |
| Topic | Pattern avec wildcards (* et #) | commande.* — stock-service et notif-service s'abonnent chacun |
# 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
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.
- Les 4 étapes obligatoires amqplib :
connect,createChannel,assertExchange,assertQueue producer.js: publier un message JSON dans RabbitMQconsumer.js: s'abonner, recevoir,ack()etnack()- Module partage
shared/rabbitmq.jsavec reconnexion automatique - Integrer la publication dans
commande-serviceet testernotif-service
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).
| Etape | Methode amqplib | Role | Producer ou Consumer ? |
|---|---|---|---|
| 1 | amqplib.connect(url) | Connexion TCP au broker RabbitMQ | Les deux |
| 2 | connexion.createChannel() | Canal logique sur la connexion (moins couteux qu'une nouvelle connexion) | Les deux |
| 3 | channel.assertExchange(nom, type, opts) | Declarer l'exchange — le creer s'il n'existe pas (idempotent) | Les deux |
| 4a | channel.publish(exchange, key, buffer) | Publier un message | Producer uniquement |
| 4b | channel.assertQueue(nom, opts) | Declarer la queue | Consumer uniquement |
| 4c | channel.bindQueue(queue, exchange, key) | Lier la queue a l'exchange | Consumer uniquement |
| 4d | channel.consume(queue, callback) | S'abonner et recevoir les messages | Consumer uniquement |
| ack | channel.ack(msg) | Confirmer le traitement — RabbitMQ supprime le message | Consumer uniquement |
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
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.
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()
});
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.
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.
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();
- Demarrer le consumer (terminal 1), puis le producer (terminal 2) → message recu immediatement.
- Arreter le consumer (Ctrl+C), publier 3 messages. Interface web :
queue-notifsaffiche 3 messages en attente. - Relancer le consumer → les 3 messages sont immediatement consommes. Aucun message perdu.
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.
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 };
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 } });
Completer le tableau sur le cahier sans consulter les notes. Durée : 3 minutes.
| Etape | Methode amqplib | Role | Qui ? |
|---|---|---|---|
| 1 | amqplib.___(url) | Connexion TCP au broker | Les deux |
| 2 | connexion.___() | Canal logique | Les deux |
| 3 | channel.assertExchange() | Declarer l'exchange | Les deux |
| 4a | channel.___(exchange, key, buffer) | Envoyer un message | Producer |
| 4b | channel.assertQueue() | Declarer la queue | Consumer |
| 4c | channel.___(queue, exchange, key) | Lier queue a exchange | Consumer |
| 4d | channel.___(queue, callback) | S'abonner | Consumer |
| ack | channel.___(msg) | Confirmer traitement | Consumer |
connect, createChannel, publish, bindQueue, consume, ack
shared/rabbitmq.js, publierEvenement() retourne false si RabbitMQ est indisponible plutot que de lever une exception. Pourquoi ?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.
- 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.confpour les 4 services - TP consolidation Partie 3 : 15 scenarios flux complet + tests de tolerance aux pannes
| Type | Qui est protege ? | Qui ne voit pas quoi ? | Exemple |
|---|---|---|---|
| Forward proxy | Le client | Le serveur ne voit pas l'IP reelle du client | VPN d'entreprise, proxy scolaire |
| Reverse proxy | Le serveur | Le client ne connait pas l'adresse des backends | Nginx 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-Optionssans toucher a Node.js.
| Critere | Gateway Express (Node.js) | Nginx |
|---|---|---|
| Performance | ~50 Mo RAM, event loop Node.js | ~2 Mo RAM, C natif — 50 000+ connexions simultanees |
| Configuration | Code JavaScript — require, app.use() | Fichier nginx.conf declaratif — pas de code |
| Logique metier | Peut executer du JS (JWT, transformation) | Pas de logique metier — routage pur |
| SSL/TLS | Verbeux (https.createServer + cert) | Natif — 3 lignes dans nginx.conf |
| Cas d'usage ideal | Prototype, logique de routage complexe | Production, haute charge, SSL, rate limiting |
# 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
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;
}
}
}
# 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
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
| # | Requete | Token | Attendu |
|---|---|---|---|
| 1 | POST /auth/register nadia@artisan.ma | — | 201 |
| 2 | POST /auth/login nadia → token_nadia | — | 200 + token |
| 3 | GET /auth/verify | token_nadia | 200 + { role: 'user' } |
| 4 | GET /produits | aucun | 200 (route publique) |
| 5 | POST /produits (creer huile argan) | token_nadia | 201 — copier l'_id |
| 6 | DELETE /produits/{id} | token_nadia | 403 (user ne peut pas supprimer) |
| 7 | POST /commandes (1 produit) | token_nadia | 201 + notification terminal notif-service |
| 8 | POST /commandes (produit stock=0) | token_nadia | 409 Conflict |
| 9 | POST /commandes (sans token) | — | 401 |
| 10 | GET /commandes | token_nadia | 200 + 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.
proxy_set_header Authorization $http_authorization est-il critique ?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.
- 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
- 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.
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.
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.
| Critere | Machine Virtuelle | Conteneur Docker |
|---|---|---|
| Taille | 5–20 Go (OS complet) | 50–500 Mo (kernel partage) |
| Demarrage | 1–5 minutes (boot OS) | < 1 seconde (processus) |
| Isolation | Totale — kernel propre, hyperviseur | Forte — namespaces, kernel partage |
| OS | OS complet par VM | Pas d'OS — kernel Linux partage |
| Performance | 5–15% overhead (hyperviseur) | < 1% overhead (namespaces natifs) |
| Cas d'usage | Isolation maximale, OS differents, legacy | Microservices, CI/CD, Cloud Native |
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.
- 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.ymldecrit toute l'architecture. Nouveau developpeur :git clone + docker-compose up→ application complete en < 5 minutes.
| Outil | Role | Dans ce module |
|---|---|---|
| Docker | Creer, executer, distribuer des conteneurs | Seances 25–29 — outil principal |
| Docker Compose | Orchestrer des architectures multi-conteneurs | Seances 28–29 |
| Podman | Alternative Docker sans daemon root | Non couvert — memes concepts |
| Kubernetes | Orchestrateur grande echelle (scaling, auto-healing) | Non couvert dans ce module |
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.
- 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
# 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
- Cherche l'image 'hello-world' localement → introuvable
- Docker pull : telecharge depuis Docker Hub
- Docker create : cree un conteneur depuis l'image
- Docker start : demarre le conteneur
- Le conteneur affiche 'Hello from Docker!' et s'arrete
| Concept | Definition | Exemple |
|---|---|---|
| Image | Snapshot immuable : code + runtime + dependances. Analogie : classe POO, moule a gateau. | node:18-alpine, mongo:7.0 |
| Conteneur | Instance en cours d'execution d'une image. Analogie : objet instancie depuis une classe. | docker run node:18-alpine |
| Registry | Serveur de stockage d'images. Public : Docker Hub. Prive : Azure Container Registry. | hub.docker.com |
| Tag | Version d'une image. latest = non deterministe en production — a eviter. | node:18-alpine, node:20-slim |
| Layer | Couche du systeme de fichiers. Chaque instruction Dockerfile = une couche. Partagees entre images. | docker image history node:18-alpine |
| Docker Hub | Registry public officiel avec les images officielles node, mongo, nginx, rabbitmq... | hub.docker.com/_/node |
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.
# 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
| Option | Role | Exemple |
|---|---|---|
-p HOST:CONT | Mapper un port hote vers le conteneur. Sans -p : inaccessible depuis l'exterieur. | -p 3001:3001 |
-e CLE=VAL | Variable d'environnement. Remplace le fichier .env. | -e JWT_SECRET=secret |
-d | Detached : lancer en arriere-plan. | docker run -d ... |
--name NOM | Nommer le conteneur (sinon nom aleatoire). | --name auth-service |
-v HOTE:CONT | Monter un dossier hote dans le conteneur. Hot reload en dev, persistance de donnees. | -v $(pwd)/auth:/app |
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
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).
Ecrire chaque commande docker run complete sur le cahier. Duree : 4 minutes.
- 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. - Ouvrir un shell interactif dans le conteneur 'produit-service'.
- Lister tous les conteneurs (actifs ET arretes).
- Arreter et supprimer 'auth-service' en une seule commande.
- Lancer MongoDB en arriere-plan, port 27017, variables MONGO_INITDB_ROOT_USERNAME=admin et MONGO_INITDB_ROOT_PASSWORD=secret, image mongo:7.0.
- 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
-p 3001:3001, que se passe-t-il quand on accede a http://localhost:3001 ?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.
- 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
.dockerignorepour exclure node_modules du contexte de build - TP : Dockerfile de produit-service + 10 tests de validation Postman
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.
| Instruction | Role | Detail important |
|---|---|---|
FROM node:18-alpine | Image de base — premiere instruction obligatoire | alpine = ~50 Mo vs ~1 Go pour node:18 |
WORKDIR /app | Definir le repertoire de travail dans le conteneur | Cree /app si inexistant. Sans WORKDIR : fichiers dans / (racine) — mauvaise pratique |
COPY package*.json ./ | Copier les manifestes de dependances | Le glob * capture package.json ET package-lock.json. Destination ./ = /app/ (WORKDIR actif) |
RUN npm install --production | Executer une commande pendant le BUILD | --production : n'installe pas les devDependencies → image plus legere. Cree une couche node_modules |
ENV NODE_ENV=production | Variable d'environnement par defaut | Accessible via process.env. Surchargeble avec docker run -e |
EXPOSE 3001 | Documenter le port d'ecoute | N'OUVRE PAS le port — informatif. Le mapping se fait avec -p |
CMD ["node", "index.js"] | Commande de demarrage par defaut | Exec form (tableau) recommandee — evite un shell intermediaire. Une seule CMD par 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
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
auth-service/routes/auth.js→ ajouterconsole.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
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.
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.
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
En s'appuyant sur le Dockerfile de auth-service comme modele, ecrire seul :
produit-service/.dockerignoreproduit-service/Dockerfile— image node:18-alpine, port 3002, NODE_ENV=production, commande node index.js
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"]
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
docker ps→ auth-service et produit-service affichent Runningdocker logs produit-service→ message de demarrage visible- GET http://localhost:3002/health → 200
- GET http://localhost:3002/produits → 200 (liste — route publique)
- POST http://localhost:3001/auth/login → 200 — recuperer token
- POST http://localhost:3002/produits avec token → 201
- GET http://localhost:3002/produits/:id → 200
- DELETE http://localhost:3002/produits/:id sans token → 401
docker exec -it produit-service sh→ls /app→ code source dans l'imagedocker exec produit-service node --version→ v18.x.x (version exacte de l'image)
① ________ node:18-alpine
② ________ /app
③ ________ package*.json ./
RUN npm install ④ ________
⑤ ________ . .
ENV NODE_ENV=production
ENV PORT=3003
EXPOSE 3003
⑥ ________ ["node", "index.js"]
① FROM ② WORKDIR ③ COPY ④ --production ⑤ COPY ⑥ CMD
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.
- Traduction de
docker runen YAMLdocker-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.ymlcomplet auth + produit + commande + MongoDB
| Option docker run | Cle docker-compose.yml | Exemple |
|---|---|---|
--name auth-service | Nom du service (cle sous services:) | auth-service: |
-p 3001:3001 | ports: | - "3001:3001" |
-e PORT=3001 | environment: | PORT: 3001 |
bdms-auth:1.0.0 (image) | image: | image: bdms-auth:1.0.0 |
| Pas d'equivalent direct | build: | context: ./auth-service |
-v mongo_data:/data/db | volumes: | - mongo_data:/data/db |
--network bdms-network | networks: | - bdms-network |
| Lancer apres mongodb | depends_on: | - mongodb |
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
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.
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
Completer les 5 blancs. Bonus : expliquer pourquoi notif-service n'a pas de cle ports:.
notif-service:
① ________ :
context: ./notif-service
container_name: bdms-notif
② ________ :
RABBITMQ_URL: amqp://guest:guest@rabbitmq:5672
EXCHANGE_NOM: commandes
③ ________ :
- rabbitmq
④ ________ :
- bdms-network
restart: ⑤ ________
- ①
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 ?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.
- 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
.envpour separer les secrets du docker-compose.yml - Reseaux multi-couches (frontend/backend) et healthcheck
| Type | Isolation | Cas d'usage | Commande |
|---|---|---|---|
| Bridge | Reseau prive isole — DNS interne entre conteneurs du meme reseau | Developpement local, Docker Compose (chaque projet a son reseau) | docker network create --driver bridge |
| Host | Partage le reseau de la machine hote — pas d'isolation | Performance maximale, Linux uniquement (pas Docker Desktop) | docker run --network=host |
| Overlay | Reseau distribue multi-hotes | Production a grande echelle, Docker Swarm, Kubernetes | Non couvert dans ce module |
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 :
- Le DNS interne Docker recoit la demande de resolution de 'mongodb'
- Il cherche un conteneur nomme 'mongodb' dans le meme reseau
- Il retourne l'IP interne du conteneur MongoDB (ex : 172.18.0.2)
- 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.
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
| Type | Syntaxe | Stockage | Cas d'usage |
|---|---|---|---|
| Volume nomme | - mongo_data:/data/db | Gere par Docker (/var/lib/docker/volumes/) | Donnees persistantes : MongoDB, uploads |
| Bind mount | - ./logs:/app/logs | Dossier exact de l'hote (chemin absolu ou relatif) | Dev (hot reload), logs, configs (:ro) |
| tmpfs | tmpfs: /tmp | Memoire RAM — jamais ecrit sur disque | Donnees temporaires sensibles, sessions |
# 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
Fichier .env — separer les secrets
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.
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
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; }
docker-compose stop et docker-compose down ?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.
- 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
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:
- Erreur 1 —
localhostdans 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-backendde nginx.
docker-compose config # Valide le YAML et affiche la config resolue (.env inclus)
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 :
docker-compose ps→ 7 services affichent 'Up'docker-compose logs mongodb | grep 'Waiting for connections'docker-compose logs rabbitmq | grep 'Server startup complete'- GET http://localhost/health → 200 { nginx: ok }
- GET http://localhost/auth/health → 200 (via Nginx)
- GET http://localhost/produits → 200 (liste)
docker network ls | grep bdms→ bdms-frontend ET bdms-backend presentsdocker volume ls | grep bdms→ mongo_data et rabbitmq_data presentsdocker-compose exec nginx ping auth-service→ PING reussi (meme reseau frontend)docker-compose exec nginx ping mongodb→ ECHEC (reseaux differents — isolation validee)
| Scenario | Action | Resultat attendu |
|---|---|---|
| Sc 1 : Arreter notif-service | POST /commandes → noter le code HTTP | 201 (commande creee). queue-notifs : 1 message en attente. Au redemarrage notif-service : consomme immediatement. |
| Sc 2 : Arreter produit-service | POST /commandes et GET /produits | 503 pour les deux (via Nginx : error_page 502 503) |
| Sc 3 : Arreter auth-service | POST /commandes et GET /produits | POST /commandes : 503. GET /produits : 200 (route publique — ne contacte pas auth-service) |
| Sc 4 : Modifier .env (AUTH_PORT=3011) | docker-compose up -d auth-service | auth-service ecoute sur 3011. Nginx contacte toujours auth-service:3001 → 502 |
| Sc 5 : Scaler commande-service × 3 | docker-compose up -d --scale commande-service=3 | Erreur 'port already in use' — les 3 instances essaient de binder le port 3003. Solution : supprimer ports: de commande-service |
| Sc 6 : down puis up | docker-compose down puis up -d | GET /produits : donnees presentes (volumes mongo_data non supprimes par down). down -v les supprimerait. |
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.
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);
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 ?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.
- 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
| Cloud | Fournisseur | Part 2024 | Points forts |
|---|---|---|---|
| AWS | Amazon | ~32% | Leader — plus grand catalogue de services, pionnier |
| Azure | Microsoft | ~22% | Integration Microsoft (Office 365, Active Directory, Windows Server) |
| GCP | ~11% | Big Data, Machine Learning, Kubernetes (GKE — cree par Google) |
| Service Azure | Role | Equivalent cloud-agnostique |
|---|---|---|
| Azure Container Instances (ACI) | Deployer un conteneur Docker en quelques secondes — facturation a la seconde | AWS Fargate |
| Azure Container Registry (ACR) | Registry prive pour les images Docker | Docker Hub (prive), ECR |
| Azure App Service | Heberger une app Node.js — scaling automatique | AWS Elastic Beanstalk, Heroku |
| Azure Cosmos DB | NoSQL managee — API MongoDB compatible | MongoDB Atlas, AWS DocumentDB |
| Azure Service Bus | Broker de messages managee — similaire a RabbitMQ | AWS SQS/SNS, RabbitMQ Cloud |
| Azure Pipelines | CI/CD — construire, tester, deployer automatiquement | GitHub Actions, GitLab CI |
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.
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.
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.
| Niveau | Nom | Role | Exemple module |
|---|---|---|---|
| 1 (haut) | Tenant (Azure AD) | L'organisation — contient utilisateurs et groupes | monentreprise.onmicrosoft.com |
| 2 | Abonnement | Unite de facturation — une facture mensuelle | Azure Free — 200$ de credit |
| 3 | Groupe de ressources | Conteneur logique — supprimer le groupe supprime tout | rg-bdms-dev (France Centre) |
| 4 (bas) | Ressource | Element Azure individuel — dans un et un seul groupe | ACI auth-service, ACR bdms |
- 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-devsupprime TOUTES les ressources du groupe.
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.
# 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
- 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/
az group delete --name rg-bdms-dev --yes. Quelles ressources sont supprimees ?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.
- 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
| Service | Type | Usage | Utilise en Partie 5 |
|---|---|---|---|
| Azure Virtual Machines | IaaS | VM Linux/Windows — controle total de l'OS | Non (trop bas niveau) |
| Azure App Service | PaaS | Heberger des apps Node.js, Python, .NET sans gerer de serveur | Seances 31-34 |
| Azure Container Instances | CaaS | Deployer un conteneur Docker — facturation a la seconde | Panorama seance 32 |
| Azure Kubernetes Service | CaaS | Orchestrer des centaines de conteneurs — K8s manage | Panorama seance 31 |
| Azure Cosmos DB | NoSQL | API MongoDB compatible — remplace MongoDB local | Alternative presentee |
| Azure Container Registry | Registry | Registry prive pour images Docker — alternative Docker Hub | Seances 32-34 |
| Azure Pipelines | CI/CD | Build, test, deploiement automatise sur git push | Seances 35-36 |
| Azure Monitor | Observabilite | Metriques, logs, alertes de toutes les ressources | Seance 34 |
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.
| Plan | RAM | CPU | Prix/mois | Limites cles | Usage |
|---|---|---|---|---|---|
| F1 (Free) | 1 Go | Partage | Gratuit | 60 min CPU/jour, pas d'Always On, pas de domaine custom | Tests et apprentissage uniquement |
| B1 (Basic) | 1.75 Go | 1 coeur | ~13 $/mois | Pas d'autoscaling, pas de slots de deploiement | Premier deploiement production |
| S1 (Standard) | 1.75 Go | 1 coeur | ~70 $/mois | 5 slots, autoscaling jusqu'a 10 instances | Production standard |
| P1v3 (Premium) | 1.75 Go | 2 coeurs | ~140 $/mois | Jusqu'a 30 instances | Production haute charge |
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.
| Critere | Mode code | Mode conteneur |
|---|---|---|
| Dockerfile requis | Non — Azure detecte le runtime | Oui — image pre-construite |
| Deploiement | git push, ZIP deploy, Azure Pipelines | az webapp config container set (tag de l'image) |
| Comportement | Depend des runtimes supportes par Azure | Identique au local — meme image |
| Mise a jour | Redeploi du code | Changer le tag de l'image — sans redeploi de code |
| Usage recommande | App simple Node.js sans Docker | Architecture microservices conteneurisee |
| Critere | Docker Compose | ACI | AKS |
|---|---|---|---|
| Environnement | Developpement local | Dev / Staging cloud | Production a grande echelle |
| Scaling | Manuel (--scale) — conflit port | Manuel (nouvelles instances ACI) | Automatique (HPA) |
| Self-healing | restart: on-failure seulement | Redemarrage si crash | Remplace pods + noeuds defaillants |
| Load balancing | Nginx manuel | Non natif | Service K8s integre |
| Cout | Gratuit (local) | Facturation a la seconde | Cout VMs + overhead (~70$/mois min) |
| Complexite | Faible | Moyenne | Elevee |
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.
# 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
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
# 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
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
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.
- 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
:latesten 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
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.
| Registry | Type | Acces | Cas d'usage | Ce module |
|---|---|---|---|---|
| Docker Hub | Public + prive | Internet — gratuit (public) | Images officielles, projets open source, partage equipe | Seance 32 |
| Azure Container Registry | Prive | Azure AD — securise | Images privees d'entreprise, integration ACI/AKS | Seances 33-34 |
| GitHub Container Registry | Public + prive | Token GitHub | Images liees a un depot GitHub, CI/CD GitHub Actions | Non couvert |
| Registry local | Prive self-hosted | Reseau local | Air-gapped, sans acces Internet | Non couvert |
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)
- MAJEUR.MINEUR.PATCH :
1.0.0→ changement incompatible = 2.0.0, nouvelle fonctionnalite = 1.1.0, correction = 1.0.1 :latestchange a chaquedocker push— pointe toujours vers la derniere image. Si une nouvelle version buguee est publiee, tous les deploiements utilisant:latestla recuperent automatiquement.:1.0.0est immuable — garantit le meme comportement a chaque deploiement.- Strategie : toujours taguer
moncompte/bdms-auth:1.0.0ETmoncompte/bdms-auth:latest. Utiliser:1.0.0en production,:latestuniquement en dev.
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
# 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
# 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.
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
docker push retourne 'Layer already exists' pour toutes les couches. Que s'est-il passe ?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.
- 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 dedocker logs -f - TP : deployer bdms-produit sur App Service — 15 tests de validation
Etapes via le portail Azure
- Ouvrir https://portal.azure.com → App Services → Creer → Application web
- Abonnement : Azure Free · Groupe : rg-bdms-dev · Nom : bdms-auth-dev
- Publier : Conteneur Docker ← choisir cette option (pas Code)
- Systeme d'exploitation : Linux · Region : France Centre
- Onglet Docker : Conteneur unique · Source : Docker Hub · Acces : Public · Image : VOTRE_USERNAME/bdms-auth:1.0.0
- Plan : F1 (gratuit) → Verifier + creer
- Attendre ~2 minutes → URL HTTPS automatique : https://bdms-auth-dev.azurewebsites.net
# 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
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.
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.
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).
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
# 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
# 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
| # | Test | Attendu |
|---|---|---|
| T1 | az webapp list --resource-group rg-bdms-dev --output table | bdms-auth-dev et bdms-produit-dev affichent Running |
| T2 | az webapp show --name bdms-auth-dev ... --query state | Running |
| T3 | az webapp log tail --name bdms-auth-dev ... | Logs visibles en temps reel |
| T4 | GET https://bdms-auth-dev.azurewebsites.net/health | 200 { service: 'auth-service' } |
| T5 | GET https://bdms-produit-dev.azurewebsites.net/health | 200 { service: 'produit-service' } |
| T6 | POST .../auth/register | 201 ou 500 (normal si MongoDB non connecte) |
| T7 | GET .../produits | 200 (route publique) |
| T8 | az webapp config appsettings list --name bdms-produit-dev | WEBSITES_PORT=3002 visible |
| T9 | Portail → bdms-produit-dev → Configuration → AUTH_SERVICE_URL | URL Azure de auth-service |
| T10 | az webapp config container show --name bdms-auth-dev ... | image: VOTRE_USERNAME/bdms-auth:1.0.0 |
| T11 | az webapp config container set ... --docker-custom-image-name .../bdms-auth:latest | App Service redemarrage avec nouvelle image |
| T12 | az webapp restart --name bdms-auth-dev ... | Redemarrage et recovery automatique |
| T13 | az webapp log tail apres restart | Redemarrage visible dans les logs |
| T14 | Portail → Surveillance → Metriques → CPU + Http 2xx | Graphiques visibles |
| T15 | az group delete --name rg-bdms-dev --yes --no-wait | Suppression de toutes les ressources |
| Critere | Mode code | Mode conteneur | ACI |
|---|---|---|---|
| Dockerfile requis | Non | Oui | Oui |
| Image Docker Hub | Non | Oui — pull auto | Oui — pull auto |
| URL HTTPS auto | Oui | Oui | Oui (IP publique) |
| Always On | B1+ uniquement | B1+ uniquement | Toujours actif |
| Facturation | Mensuelle fixe | Mensuelle fixe | A la seconde |
| Mise a jour image | N/A | az webapp config container set | az container create (recreer) |
| Logs | az webapp log tail | az webapp log tail | az container logs |
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.
- Differences entre
docker-compose.azure.ymlet 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
| Fonctionnalite | docker-compose.yml local (Partie 4) | docker-compose.azure.yml (Azure) |
|---|---|---|
build: | Construit l'image depuis un Dockerfile | Interdit — uniquement images pre-construites (image:) |
| Volumes bind mount | Monter ./logs:/app/logs | Non supporte — utiliser le stockage Azure |
depends_on condition: | service_healthy fonctionne | Non supporte — simplifie |
| Port expose | Tous les services peuvent exposer des ports | Un seul service expose un port vers l'exterieur |
| DNS interne | Nom de service Compose = hostname | Meme principe — nom de service = hostname DNS Azure |
| Secrets | Variables en clair dans le YAML ou .env | Variables referencees avec ${VAR} — injectees via App Settings Azure |
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'
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
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.
| Outil | Acces | Usage | Equivalent local |
|---|---|---|---|
| Log Stream | Portail : Surveillance → Flux de journaux · CLI : az webapp log tail | Logs en temps reel — deboguer un probleme en direct, observer le demarrage des conteneurs | docker logs -f ou docker-compose logs -f |
| App Diagnostics | Portail : Diagnostiquer et resoudre les problemes | Analyse automatique : taux d'erreur, problemes de conteneur, erreurs HTTP 4xx/5xx avec trace complete | docker inspect + lecture manuelle des logs |
| Metriques | Portail : Surveillance → Metriques | CPU, memoire, Http 2xx/5xx, temps de reponse, nombre de requetes — graphiques et alertes | docker 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
# 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
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.
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
| # | Test | Attendu |
|---|---|---|
| T1 | az webapp list --resource-group rg-bdms-final --output table | bdms-final-VOTRE_NOM affiche Running |
| T2 | az webapp log tail | Logs des 3 services (auth, produit, commande) visibles |
| T3 | Portail → Diagnostiquer et resoudre → aucune alerte critique | Sante app OK |
| T4 | GET https://bdms-final-VOTRE_NOM.azurewebsites.net/health | 200 (service principal) |
| T5 | az webapp config appsettings list --name bdms-final-VOTRE_NOM | MONGO_URI et JWT_SECRET presents |
| T6 | POST .../auth/register { email, motDePasse, nom } | 201 (si MongoDB connecte) |
| T7 | POST .../auth/login → recuperer token_azure | 200 + token JWT |
| T8 | GET .../auth/verify avec token_azure | 200 { valide: true } |
| T9 | GET .../produits | 200 (route publique) |
| T10 | POST .../produits avec token_azure { nom, prix, stock, categorie } | 201 + _id du produit |
| T11 | GET .../produits/:id | 200 + champ disponible calcule |
| T12 | POST .../commandes avec token_azure { produitId, quantite } | 201 commande creee |
| T13 | GET .../commandes avec token_azure | 200 liste des commandes |
| T14 | Portail → Metriques → CPU Percentage | Graphique visible, pic apres les tests Postman |
| T15 | Portail → Metriques → Http 2xx | Pic correspond aux requetes Postman |
| T16 | az webapp log download ... --log-file logs.zip | Fichier logs.zip telecharge |
| T17 | az webapp config container set ... --multicontainer-config-file docker-compose.azure.yml | Redeploiement sans downtime |
| T18 | az webapp log tail pendant T17 | Redemarrage des 3 conteneurs visible |
| T19 | az webapp config appsettings set ... --settings NODE_ENV=staging | Variable mise a jour sans rebuild ni recreer l'App Service |
| T20 | az group delete --name rg-bdms-final --yes --no-wait | Suppression de toutes les ressources — nettoyage final |
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
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 ?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.
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.
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)
| Service | Port | Responsabilite | Base |
|---|---|---|---|
| auth-service | 3001 | Register, login, /verify — JWT + bcrypt | bddarigo-auth |
| cuisine-service | 3002 | CRUD restaurants, CRUD plats, disponibilite | bddarigo-cuisines |
| commande-service | 3003 | Creer commande, orchestrer auth+cuisine, publier evenement RabbitMQ | bddarigo-commandes |
| livraison-service | 3004 | Consumer RabbitMQ, affecter livreur, suivre statut | bddarigo-livraisons |
| notif-service | — | Consumer RabbitMQ, enregistrer toutes les notifications | bddarigo-notifs |
| Nginx | 80 | Reverse proxy, routing, rate limiting sur /auth/login | — |
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 ..
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
node_modules/
.env
.env.local
*.log
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
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.
- Connexion MongoDB avec
config/mongodb.js(connecter + getBase) POST /auth/register: validation, hashage bcrypt, stockagePOST /auth/login: bcrypt.compare + jwt.signGET /auth/verify: endpoint inter-services pour valider un token sans partager JWT_SECRETshared/authClient.js: middlewareisAuthenticatedreutilisable par tous les services
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 };
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);
});
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;
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 };
- 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
GET /auth/verify au lieu de partager JWT_SECRET avec tous les services ?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.
- 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
disponiblesur chaque plat selonstock > 0 - Pagination skip/limit sur la liste des restaurants
- Protection des routes avec
isAuthenticatedetisRoledepuis shared/
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); });
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;
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;
- 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
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.
POST /commandes: orchestrer auth + cuisine, valider stock, creer commande, publier evenement RabbitMQGET /commandes: liste des commandes de l'utilisateur connecteGET /commandes/:id: detail d'une commandePATCH /commandes/:id/statut: mettre a jour le statutshared/rabbitmq.js: module singleton de publication
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 };
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;
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); });
nomPlat et prixUnitaire directement dans le document. Pourquoi ne pas stocker uniquement platId et lire cuisine-service a chaque consultation ?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.
- Consumer RabbitMQ : ecouter l'evenement
commande.creeeet creer une mission de livraison GET /livraisons: liste des missions du livreur connectePATCH /livraisons/:id/accepter: un livreur accepte une missionPATCH /livraisons/:id/statut: le livreur met a jour le statut (en_route, livree)
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); });
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.
- 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
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); });
# 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
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).
- 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
Authorizationvers 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
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"}'; }
}
}
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.
sudo nginx -t # Tester la syntaxe — TOUJOURS avant
sudo nginx -s reload # Recharger sans downtime
- 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
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.
- Dockerfile identique pour les 5 services Node.js (node:18-alpine, ordre cache optimal)
.dockerignorepour exclure node_modules et .envdocker-compose.ymlcomplet : 7 services, reseaux multi-couches, healthchecks, .env- Reseaux :
darigo-frontend(Nginx + services) etdarigo-backend(services + MongoDB + RabbitMQ) - Validation : 20 tests de demarrage et flux complet
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"]
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.
node_modules
.env
.env.*
*.log
.git
README.md
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
cd darigo
docker-compose up --build -d # Build + demarrage
docker-compose ps # 7 services Running
docker-compose logs -f # Suivre tous les logs
| # | Test | Attendu |
|---|---|---|
| T1 | docker-compose ps | 7 services Up |
| T2 | GET http://localhost/nginx-health | 200 { statut: actif } |
| T3 | GET http://localhost/auth/health | 200 via Nginx → auth-service |
| T4 | GET http://localhost/cuisines/ | 200 liste restaurants |
| T5 | POST /auth/register (role client) → token_client | 201 |
| T6 | POST /auth/register (role restaurateur) → token_resto | 201 |
| T7 | POST /auth/register (role livreur) → token_livreur | 201 |
| T8 | POST /auth/login → tokens pour chaque role | 200 + JWT |
| T9 | POST /cuisines/ avec token_resto → restaurantId | 201 |
| T10 | POST /cuisines/:id/plats (3 plats) avec token_resto | 201 × 3 |
| T11 | GET /cuisines/:id/plats → 3 plats avec disponible:true | 200 |
| T12 | POST /commandes avec token_client → commandeId | 201 + total |
| T13 | Logs notif-service : SMS envoye | Message consomme immediatement |
| T14 | Logs livraison-service : Mission creee | Mission creee depuis RabbitMQ |
| T15 | GET /livraisons avec token_livreur → mission disponible | 200 + liste |
| T16 | PATCH /livraisons/:id/accepter avec token_livreur | 200 Mission acceptee |
| T17 | PATCH /livraisons/:id/statut { statut: en_route } token_livreur | 200 |
| T18 | PATCH /livraisons/:id/statut { statut: livree } | 200 |
| T19 | docker-compose exec nginx ping mongodb | ECHEC — isolation reseau validee |
| T20 | Arreter notif-service → 3 commandes → relancer → 3 messages | Resilience RabbitMQ confirmee |
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.
- 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
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
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
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
| # | Requete sur https://darigo-app.azurewebsites.net | Attendu |
|---|---|---|
| T1 | GET /auth/health | 200 { service: auth-service } |
| T2 | POST /auth/register (client) | 201 + userId |
| T3 | POST /auth/register (restaurateur) | 201 |
| T4 | POST /auth/login client → token_client | 200 + JWT |
| T5 | POST /auth/login restaurateur → token_resto | 200 + JWT |
| T6 | POST /cuisines/ avec token_resto | 201 + restaurantId |
| T7 | POST /cuisines/:id/plats (couscous, tajine, pastilla) | 201 × 3 |
| T8 | GET /cuisines/ → restaurant visible | 200 |
| T9 | POST /commandes avec token_client + platId + quantite | 201 + total |
| T10 | GET /commandes avec token_client | 200 + historique |
| T11 | az webapp log tail → logs des 5 services visibles | Tous actifs |
| T12 | Portail → Metriques → CPU + HTTP 2xx apres tests | Graphiques avec pics |
| T13 | az webapp restart darigo-app → GET /auth/health | Recovery automatique |
| T14 | az group delete --name rg-darigo-prod --yes --no-wait | Nettoyage final |
- 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
ports: mais les autres services ont WEBSITES_PORT. Pourquoi cette difference ?