Summary
[!note] 📚 Série Smart Assets Manager
- Pourquoi l’Abstraction de Stockage Est Importante ← vous êtes ici
- Quatre Backends, Une Interface — 18 mai
- L’API Unifiée : Crédits et Limitation de Débit — 27 avril
- Stratégie de Tests : Unitaires vs E2E — 20 avril
- 5 Cas Limites qui Brisent les APIs d’Images — 1er juin
- Documentation API : Swagger + Postman — 30 mars
Quand j’ai commencé à construire Smart Assets Manager — un système pour générer en masse des images de couverture de blog, des icônes d’app store, et des visuels pour les réseaux sociaux — la question du stockage semblait simple. Générer l’image, la téléverser sur Cloudinary, retourner l’URL. Terminé.
Ça a duré environ deux semaines.
La troisième personne à utiliser l’API est revenue avec une question : « Est-ce que je peux utiliser ça localement pendant le développement sans avoir besoin d’identifiants Cloudinary ? » Bonne question. La réponse, avec un backend Cloudinary codé en dur, était non.
Une semaine plus tard, une demande d’intégration CI/CD est arrivée : l’API était appelée en cours de pipeline pour générer des images Open Graph avant le déploiement. Téléverser sur Cloudinary dans ce contexte créait une dépendance externe susceptible d’échouer indépendamment du déploiement lui-même. L’auteur du pipeline voulait recevoir les octets de l’image directement dans la réponse et les stocker comme bon lui semblait.
Deux semaines après, un cas d’utilisation entreprise est apparu : un client dont la politique de résidence des données interdisait que les images quittent son infrastructure. Cloudinary, en tant que CDN tiers, était totalement exclu.
Trois problèmes différents. Trois exigences différentes. Tous pointant vers la même cause profonde : le générateur et le stockage étaient couplés.
À retenir : Un backend de stockage codé en dur dans une API de génération d’images n’est pas une décision de stockage — c’est une contrainte qui se propage à travers chaque cas d’utilisation auquel vous n’avez pas encore pensé. La question n’est pas de savoir si vous aurez besoin de flexibilité ; c’est de savoir si vous l’aurez construite avant d’en avoir besoin.
Ce que Coûte le Couplage
Le code de l’époque ressemblait approximativement à ceci — le générateur produit des octets, l’appel Cloudinary produit une URL, l’URL est retournée :
def generate_and_store(template_data: dict, width: int, height: int) -> str:
image_bytes = renderer.render(template_data, width, height)
# Upload directly to Cloudinary — no abstraction
result = cloudinary.uploader.upload(image_bytes, public_id=template_data["name"])
return result["secure_url"]
Propre, simple, fonctionne parfaitement — jusqu’à ce que l’un des trois cas d’utilisation ci-dessus apparaisse. À ce moment-là, cette fonction doit être modifiée. Chaque test qui mocke cloudinary.uploader.upload doit être mis à jour. Chaque endpoint qui appelle cette fonction hérite de Cloudinary comme dépendance. Chaque environnement CI exécutant des tests a besoin que les identifiants Cloudinary soient configurés.
Le changement nécessaire n’était pas de remplacer Cloudinary par autre chose. C’était de déplacer la décision « où va l’image » hors du code de génération et dans un paramètre que l’appelant contrôle. Le générateur doit produire des octets. Ce qui se passe avec ces octets doit être configuré séparément.
Quatre Cas d’Utilisation, Quatre Backends
Les trois demandes décrites ci-dessus, plus l’hébergement en production, ont donné la forme de l’abstraction :
| Backend | Besoin de l’appelant | Ce que fait le stockage |
|---|---|---|
| Cloudinary | Hébergement production, URLs partageables | Téléverse sur CDN, retourne l’URL publique |
| Système de fichiers local | Développement, exigences sur site | Écrit sur disque, retourne l’URL localhost |
| Amazon S3 | Charges de travail production sur AWS | Téléverse dans un bucket, retourne l’URL S3 |
| Téléchargement direct | Pipelines CI/CD, consommateurs d’API | Ignore le stockage, retourne un URI data base64 dans la réponse |
L’appelant spécifie quel backend utiliser via un paramètre storage dans la requête API. Le générateur ne change pas — seul le backend dispatché par la factory change.
Le backend de téléchargement direct mérite une note : c’était le cas d’utilisation que personne n’avait planifié et qui s’est avéré le plus utilisé par les développeurs. Au lieu de téléverser quelque part, il encode les octets bruts de l’image en base64 et les retourne en ligne dans la réponse API :
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...
Pas d’identifiants requis. Pas de coûts de stockage. Pas de latence aller-retour. Pour le développement, les tests, et les pipelines automatisés où l’image sera de toute façon traitée par le système en aval, c’est la bonne valeur par défaut — et elle n’existe que parce que l’abstraction en a fait un coût de quelques heures plutôt qu’une refactorisation.
[!note] Les backends que vous n’aviez pas prévus Le backend de téléchargement direct — retourner du base64 dans la réponse — était le backend le plus utile que personne n’avait pensé à demander dès le départ. Les abstractions de stockage se justifient en partie par la facilité avec laquelle elles permettent d’ajouter les backends que vous n’aviez pas anticipés.
Ce que l’Abstraction Coûte Réellement
Le pattern qui permet tout cela n’est pas complexe :
- Une classe de base abstraite avec deux méthodes requises —
upload()retourne une URL,generate_signed_url()retourne une URL limitée dans le temps pour les assets privés - Quatre implémentations concrètes — une classe par backend, chacune implémentant les deux mêmes méthodes
- Une fonction factory — prend le paramètre
storagede la requête, retourne l’instance de backend appropriée
C’est un seul fichier, environ 250 lignes entre la classe de base et les quatre implémentations. L’investissement est borné et concentré sur le départ. Le bénéfice — la capacité d’ajouter un nouveau backend, de remplacer le backend de production, ou de tester sans identifiants externes — se compose avec chaque fonctionnalité ajoutée au système après sa mise en place.
Rétrofitter cette abstraction après que le système avait grandi aurait nécessité de toucher le générateur, chaque endpoint API, chaque test, et chaque configuration de déploiement. Le coût de le faire dès le départ est toujours inférieur au coût de le faire plus tard. Mais la comparaison ne devient visible qu’avec le recul, ce qui est ce qui fait qu’« utilise simplement Cloudinary » semble un choix raisonnable au début.
La Suite
Le cas architectural est établi. Le prochain article couvre l’implémentation concrète : comment quatre backends de stockage partagent une interface, incluant les contrôles de confidentialité, la logique de nettoyage en cas d’échec, et la détection de signature d’octets qui rend l’inférence de format fiable.