L'architecture complète
4 APIs indépendantes qui communiquent entre elles:
┌──────────────┐
│ Frontend │ (Django/port 8004)
└──────┬───────┘
│
├──→ API Clients (Django/port 8000) → MariaDB
├──→ API Catalogue (Django/port 8001) → MariaDB
├──→ API Panier (Flask/port 8002) → Redis
└──→ API Commandes (Django/port 8003) → MariaDB
Chaque API:
- A sa propre base de données (MariaDB ou Redis)
- Tourne dans son propre container Docker
- Est déployée indépendamment sur Kubernetes
- A son propre repo Git
API Clients : authentification centralisée
C'est le point d'entrée pour tout ce qui touche aux utilisateurs.
Endpoints principaux:
POST /api/auth/register/ → Créer un compte
POST /api/auth/login/ → Se connecter (retourne un token)
GET /api/auth/validate-token/ → Valider un token
GET /api/auth/profile/ → Récupérer son profil
Modèle User:
class User(AbstractUser):
ROLE_CHOICES = [
('client', 'Client'),
('vendeur', 'Vendeur'),
('admin', 'Administrateur'),
]
role = models.CharField(
max_length=10,
choices=ROLE_CHOICES,
default='client'
)
Le truc important: les autres APIs ne gèrent PAS l'auth. Elles font juste une requête à l'API Clients pour valider le token.
Authentification externe pour les autres APIs
Voici comment j'ai fait l'auth dans les autres services:
# api/authentification.py (API Catalogue/Commandes)
class ExternalUser:
def __init__(self, user_id: int, first_name: str, last_name: str, role: str):
self.id = user_id
self.first_name = first_name
self.last_name = last_name
self.role = role
@property
def is_authenticated(self) -> bool:
return True
class ExternalTokenAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
auth_header = request.META.get("HTTP_AUTHORIZATION", "")
if not auth_header:
return None
parts = auth_header.split()
if len(parts) != 2 or parts[0] not in {"Bearer", "Token"}:
raise exceptions.AuthenticationFailed("Format invalide")
# Appel à l'API Clients
response = requests.get(
"http://apiclients:8000/api/auth/validate-token/",
headers={"Authorization": auth_header},
timeout=3
)
if response.status_code != 200:
raise exceptions.AuthenticationFailed("Token invalide")
data = response.json()
user = ExternalUser(
user_id=data['user_id'],
first_name=data['first_name'],
last_name=data['last_name'],
role=data['role']
)
return user, None
Ça me permet d'utiliser les permissions DRF normalement dans toutes les APIs.
API Catalogue : produits et avis
Gère tout ce qui touche aux produits.
Modèles principaux:
class Category(models.Model):
name = models.CharField(max_length=100, primary_key=True)
description = models.TextField()
enabled = models.BooleanField(default=True)
class Item(models.Model):
name = models.CharField(max_length=100)
description = models.TextField()
price = models.DecimalField(max_digits=10, decimal_places=2)
picture = models.ImageField(max_length=255)
stock = models.IntegerField(default=0)
average_rating = models.FloatField(default=0)
category_name = models.ForeignKey(Category, on_delete=models.CASCADE)
id_seller = models.IntegerField() # Référence vers l'API Clients
class Review(models.Model):
item = models.ForeignKey(Item, on_delete=models.CASCADE)
rating = models.FloatField()
comment = models.TextField()
id_client = models.IntegerField() # Référence vers l'API Clients
Le truc cool: Les avis mettent à jour automatiquement la note moyenne grâce aux signals Django:
# api/signals.py
from django.db.models import Avg
from django.db.models.signals import post_save, post_delete
@receiver(post_save, sender=Review)
def on_review_save(sender, instance, **kwargs):
agg = Review.objects.filter(item_id=instance.item_id).aggregate(avg=Avg('rating'))
Item.objects.filter(id=instance.item_id).update(
average_rating=round(agg['avg'] or 0.0, 2)
)
@receiver(post_delete, sender=Review)
def on_review_delete(sender, instance, **kwargs):
# Même chose pour la suppression
Gestion du stock
J'ai ajouté un endpoint spécial pour réduire le stock:
def reduce_stock(self, quantity):
if self.stock < quantity:
raise ValueError("Stock insuffisant")
self.stock -= quantity
self.save()
Cet endpoint est appelé par l'API Commandes quand une commande est validée.
API Panier : Flask + Redis
Contrairement aux autres, celle-ci est en Flask (plus léger pour un simple panier).
Stockage:
# Clé Redis: cart:<user_id>
# Valeur: JSON array d'IDs d'items
def get_cart_for_user(user_id):
cart_key = f"cart:{user_id}"
cart_data = redis_client.get(cart_key)
if cart_data:
return json.loads(cart_data)
return []
def save_cart_for_user(user_id, items):
cart_key = f"cart:{user_id}"
redis_client.set(cart_key, json.dumps(items))
Endpoints:
GET /items → Récupérer son panier
POST /items → Ajouter un item
PUT /items → Remplacer tout le panier
DELETE /items → Vider le panier
DELETE /items/<id> → Supprimer un item
Pourquoi Redis ? Parce que c'est temporaire. Si le serveur redémarre, on s'en fout de perdre les paniers.
API Commandes : l'orchestrateur
Celle-ci coordonne les autres services.
Workflow de création de commande:
def post(self, request):
# 1. Récupérer les infos des items depuis l'API Catalogue
for item_input in data['items']:
item_data = fetch_item_info(item_input['item_id'])
# 2. Vérifier et réduire le stock
stock_response = requests.post(
f"http://apicatalogue:8001/api/items/{item_id}/stock/",
json={'quantity': quantity}
)
if stock_response.status_code != 200:
return Response({'error': 'Stock insuffisant'}, 400)
# 3. Créer la commande
order = Order.objects.create(id_buyer=data['id_buyer'])
# 4. Sauvegarder les items (snapshot à l'instant T)
for item_info in items_info:
OrderItem.objects.create(
order=order,
item_id=item_info['item_id'],
name=item_info['name'], # Snapshot du nom
price=item_info['price'], # Snapshot du prix
id_seller=item_info['id_seller'],
quantity=item_info['quantity']
)
Pourquoi sauvegarder un snapshot ?
Parce que si le vendeur change le prix ou le nom du produit après, la commande doit refléter ce qui était affiché au moment de l'achat.
Génération de PDF
Bonus: chaque commande peut être exportée en PDF avec ReportLab:
from reportlab.platypus import SimpleDocTemplate, Table
from reportlab.lib.pagesizes import A4
def get(self, request, order_id):
order = Order.objects.get(id=order_id)
buffer = BytesIO()
doc = SimpleDocTemplate(buffer, pagesize=A4)
# Tableau des items
items_data = [['Article', 'Prix', 'Quantité', 'Total']]
for item in order.items.all():
items_data.append([
item.name,
f"{item.price}€",
str(item.quantity),
f"{item.price * item.quantity}€"
])
table = Table(items_data)
doc.build([table])
pdf_content = buffer.getvalue()
response = HttpResponse(pdf_content, content_type='application/pdf')
response['Content-Disposition'] = f'attachment; filename="commande_{order.id}.pdf"'
return response
Les pièges de la communication inter-services
Erreur #1: Timeout par défaut trop court
Les requests Python ont un timeout par défaut infini. J'ai ajouté des timeouts partout:
response = requests.get(url, timeout=3) # 3 secondes max
Erreur #2: Pas de gestion d'erreur
Si l'API Catalogue est down, l'API Commandes plantait. J'ai ajouté des try/catch:
try:
response = requests.get(url, timeout=3)
except requests.RequestException:
return Response({'error': 'Service indisponible'}, 502)
Erreur #3: Circular dependencies
Au début, l'API Catalogue appelait l'API Commandes qui appelait l'API Catalogue. Mauvaise idée.
Solution: Communication unidirectionnelle:
- Frontend → APIs
- API Commandes → API Catalogue (pour vérifier le stock)
- API Catalogue ← Jamais d'appel sortant
Permissions et rôles
Chaque API a ses propres permissions:
# API Catalogue: seul le vendeur peut modifier ses items
class IsItemSeller(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return request.user.id == obj.id_seller
# API Commandes: seul l'admin peut supprimer
class UserAdminPermission(permissions.BasePermission):
def has_permission(self, request, view):
return request.user.role == 'admin'
Déploiement Kubernetes
Chaque API a son deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: apicatalogue
spec:
replicas: 1
template:
spec:
initContainers:
- name: wait-for-db
image: busybox:1.36.1
command: ['sh', '-c', 'until nc -z dbcatalogue 3307; do sleep 2; done;']
containers:
- image: ghcr.io/uha-sae53/api-catalogue:latest
ports:
- containerPort: 8001
env:
- name: DB_CATALOGUE_HOST
value: "dbcatalogue"
Le initContainer attend que la base soit prête avant de lancer l'API.
Conclusion
Cette archi microservices m'a appris:
- La communication inter-services c'est chiant
- Les timeouts sont ESSENTIELS
- Un service = une responsabilité
- Les snapshots de données évitent les incohérences
C'est plus complexe qu'un monolithe, mais ça scale mieux et c'est plus fun à développer.
Code complet: [GitHub]
Top comments (0)