Aller au contenu principal
APIsBackendArchitecture

Architecture d'API REST : 7 pratiques pour que votre backend ne tombe pas en production

Andres Betancourt8 min de lecture

La plupart des API échouent en production non pas par manque de fonctionnalités, mais à cause de décisions d'architecture que personne n'a prises délibérément. Voici les sept pratiques qui séparent une API qui tient sous votre premier vrai pic de trafic de celle qui tombe le jour des soldes — et comment les prioriser si vous ne pouvez pas toutes les mettre en place d'un coup.

1. Versionnez l'API dès le premier endpoint

Un préfixe comme /api/v1/ ne coûte rien à ajouter aujourd'hui et évite de casser tous vos clients (web, mobile, intégrations) le jour où vous devrez changer un contrat. L'erreur courante est de penser « je n'en ai pas encore besoin, je l'ajouterai le moment venu » — mais quand ce moment arrive, il y a déjà des clients en production consommant la version sans préfixe, et ajouter le versionnage rétroactivement signifie soit maintenir deux routes pour la même chose, soit forcer une migration client inconfortable. Le coût de versionner dès le premier jour est quasi nul ; le coût de l'ajouter plus tard est une migration complète des clients.

2. Validez en périphérie, pas au milieu

Toute entrée externe est validée avant de toucher la logique métier — avec une librairie de schémas (Zod, Yup), pas avec des if épars dans le code. Cela apporte un bénéfice qui va au-delà de la sécurité : quand la validation vit à un seul endroit, avec un schéma explicite, la documentation de l'API s'écrit pratiquement toute seule, et n'importe qui de nouveau dans l'équipe peut voir exactement quelle forme attend chaque endpoint sans avoir à lire l'implémentation complète.

const OrderSchema = z.object({
  items: z.array(ItemSchema).min(1),
  customerId: z.string().uuid(),
});

const body = OrderSchema.parse(await req.json());

3. Des codes de statut HTTP cohérents

201 à la création, 204 à la suppression sans contenu, 409 en cas de conflit, 422 en cas d'échec de validation. Un client qui consomme votre API doit pouvoir décider à partir du seul code de statut, sans parser le message d'erreur. Cela compte particulièrement pour les intégrations automatisées (un autre système qui consomme votre API sans qu'un humain ne lise la réponse) : un webhook ou une tâche planifiée doit pouvoir décider de réessayer, d'abandonner ou d'alerter en se basant uniquement sur le code, et un backend qui renvoie 200 avec un champ `success: false` enfoui dans le corps casse cette capacité de décision automatique.

4. Authentification et autorisation séparées

L'authentification répond à « qui êtes-vous ? ». L'autorisation répond à « pouvez-vous faire ceci ? ». Les mélanger dans le même middleware est le moyen le plus courant de finir avec un endpoint qui expose des données qu'il ne devrait pas. Le schéma le plus sûr consiste à résoudre l'identité une seule fois, tôt dans la chaîne de middleware, puis à laisser chaque endpoint (ou un middleware dédié par ressource) décider de la permission spécifique dont il a besoin — ne jamais supposer qu'« être authentifié » équivaut à « pouvoir voir ceci ».

5. Le rate limiting avant d'en avoir besoin

Un endpoint sans limite de débit n'est pas seulement vulnérable aux abus — un seul client mal configuré (un cron mal écrit, un frontend en boucle) peut mettre votre backend à genoux sans que personne n'attaque quoi que ce soit. Le rate limiting n'a pas besoin d'être sophistiqué dès le départ : une limite simple par IP ou par token d'API, avec une réponse 429 claire et un en-tête `Retry-After`, couvre déjà la plupart des cas réels. L'important, c'est que cela existe avant le premier incident, pas après.

6. L'idempotence sur les opérations critiques

Si un paiement est retenté après un timeout réseau, le second appel ne doit pas facturer deux fois. Une clé d'idempotence (Idempotency-Key) dans l'en-tête résout cela sans logique complexe dans chaque endpoint : le serveur stocke le résultat de la première exécution associé à cette clé, et si une autre requête arrive avec la même clé, il renvoie le résultat stocké au lieu de ré-exécuter l'opération. C'est particulièrement critique pour tout endpoint qui déplace de l'argent, crée des ressources avec des effets externes (envoyer un e-mail, déclencher une notification) ou modifie un stock.

const existing = await getIdempotentResult(idempotencyKey);
if (existing) return existing;

const result = await processPayment(body);
await saveIdempotentResult(idempotencyKey, result);
return result;

Une erreur fréquente consiste à supposer que l'idempotence ne concerne que les paiements. Toute opération avec un effet externe qui n'est pas trivialement réversible — envoyer une notification push, déclencher un e-mail transactionnel, créer un ticket dans un système externe — bénéficie du même schéma. La règle pratique est de se demander : si cet appel s'exécute deux fois par accident, le résultat visible pour l'utilisateur change-t-il de façon notable ? Si la réponse est oui, cet endpoint a besoin de sa propre clé d'idempotence, qu'il déplace de l'argent ou non.

7. Logging et observabilité dès le premier jour

Quand quelque chose casse en production à 2h du matin, la question n'est pas « que s'est-il passé » mais « ai-je un moyen de le savoir ? ». Des logs structurés avec un ID de corrélation par requête font la différence entre un diagnostic de cinq minutes et une nuit entière à deviner. L'ID de corrélation est la pièce clé : généré au début de chaque requête et propagé à travers tous les logs, appels de services externes et files de tâches que cette requête déclenche, il permet de reconstituer le chemin complet d'une requête spécifique sans avoir à croiser des logs à la main par horodatage approximatif.

« Structuré » est le mot qui fait la différence ici : un log au format JSON avec des champs cohérents (horodatage, niveau, ID de corrélation, message, contexte additionnel) peut être interrogé, filtré et agrégé avec des outils — un log en texte libre ne peut être que lu, ligne par ligne, en espérant repérer la bonne parmi des milliers. L'investissement de transformer `console.log('erreur traitement commande')` en un log structuré avec l'ID de la commande et l'ID de corrélation est minime à l'écriture, et énorme la première fois qu'il faut enquêter sur un incident réel sous pression.

8. Pagination et filtrage cohérents

Un endpoint de listing sans pagination est une bombe à retardement : il fonctionne parfaitement avec dix enregistrements en développement et s'effondre avec cent mille en production. La convention la plus simple et la plus prévisible est la pagination par curseur (un identifiant du dernier élément vu, pas un numéro de page) pour les collections qui croissent vite, car elle évite le problème de résultats dupliqués ou sautés quand de nouveaux enregistrements sont insérés entre une page et la suivante. Le filtrage et le tri devraient suivre la même convention de paramètres de requête sur tous les endpoints de l'API (`?status=active&sort=-createdAt`), pas une convention différente par ressource — l'incohérence entre endpoints est l'une des sources les plus courantes de bugs d'intégration chez les clients qui consomment l'API.

9. CORS configuré à dessein, pas copié depuis Stack Overflow

Il est courant de voir `Access-Control-Allow-Origin: *` en production parce que « c'est ce qui a fait disparaître l'erreur en développement » — et c'est exactement le genre de configuration qui semble fonctionner jusqu'à ce qu'elle devienne un vrai problème de sécurité, surtout sur les endpoints gérant des cookies de session ou des données sensibles. La configuration correcte déclare explicitement quelles origines sont autorisées (le vrai domaine du frontend, pas un joker), quelles méthodes et en-têtes sont permis, et si les identifiants sont autorisés — chacune de ces valeurs devrait être une décision consciente, pas le premier résultat qui a fait disparaître l'erreur en local.

Comment prioriser si vous ne pouvez pas tout faire d'un coup

Sur un nouveau projet, l'ordre raisonnable est : versionnage et validation en premier (quasi gratuits et évitent une dette technique immédiate), authentification/autorisation séparées et codes de statut cohérents en second (ils affectent la conception de chaque endpoint dès le départ), pagination et CORS lors de la définition des premiers endpoints de listing (les ajouter après coup coûte plus cher que de bien les concevoir dès la première ressource), et rate limiting, idempotence et observabilité comme vague suivante, avant le premier lancement avec du trafic réel — pas après le premier incident qu'ils auraient évité.

Un schéma qui se répète dans les projets hérités d'un autre prestataire est qu'aucune de ces pratiques ne manque complètement — ce qui manque, c'est la cohérence. Un endpoint versionne et un autre non, l'un valide avec un schéma et l'autre avec des if épars, l'un pagine par curseur et l'autre par offset. Cette incohérence coûte, en pratique, presque aussi cher que l'absence totale de la pratique, car elle oblige chaque client de l'API à gérer des cas particuliers par endpoint plutôt que de supposer un contrat uniforme. La discipline d'appliquer ces sept — ou neuf — pratiques de façon uniforme sur tout le backend compte autant que chaque pratique individuelle.

La façon la plus efficace de maintenir cette cohérence dans le temps, surtout quand l'équipe grandit, est de la documenter en un seul endroit que n'importe quel nouvel arrivant peut lire avant d'écrire son premier endpoint — un guide de style d'API interne, bref, avec de vrais exemples du projet lui-même. Cela ne remplace pas la revue de code, mais cela réduit drastiquement le nombre de fois où cette revue finit par débattre de conventions de base plutôt que de la logique métier réelle du changement. Une API documentée avec OpenAPI/Swagger généré à partir des mêmes schémas de validation (plutôt que maintenu à la main séparément) remplit automatiquement ce même rôle, sans dépendre de quelqu'un qui se souvient de mettre à jour un document à part.

Aucune de ces pratiques n'est exotique — ce sont des décisions d'architecture documentées qui coûtent le même prix à bien implémenter dès le départ qu'à mal implémenter, et c'est exactement ce que nous vérifions lorsque nous auditons ou livrons une API à un client.

Retour au blog