Étiquette : API

  • Comment construire une API REST avec limitation de débit

    Comment construire une API REST avec limitation de débit

    Toute API exposée au public finira par se faire marteler, que ce soit par un client buggé qui boucle sur une erreur, un scraper, ou quelqu’un qui en abuse franchement. La limitation de débit est la façon de protéger votre service et de le garder équitable pour tout le monde. J’en ai ajouté à beaucoup d’API, et les concepts sont plus simples que le jargon le laisse croire.

    Choisissez un algorithme

    Il existe quelques approches classiques qui arbitrent entre précision et coût. La fenêtre fixe compte les requêtes par tranche de temps, disons 100 par minute, et se réinitialise à la frontière. C’est très simple mais cela autorise des rafales aux bords, puisqu’un client peut tirer 100 requêtes à la fin d’une minute et 100 au début de la suivante. La fenêtre glissante lisse cela en pondérant la fenêtre précédente. Le seau à jetons remplit des jetons à un rythme constant et laisse les clients les dépenser en rafales jusqu’à un plafond, ce qui colle le mieux au trafic réel.

    • Fenêtre fixe : la plus facile à construire, autorise des rafales aux bords.
    • Fenêtre glissante : plus précise, un peu plus de comptabilité.
    • Seau à jetons : gère les rafales avec souplesse, mon choix habituel.

    Un seau à jetons simple

    L’idée est que chaque client a un seau de jetons. Chaque requête coûte un jeton, et les jetons se rechargent avec le temps. Si le seau est vide, la requête est rejetée. Voici la logique de base, en ignorant le stockage un instant :

    function allow(bucket, now, rate, capacity) {
      const elapsed = (now - bucket.last) / 1000;
      bucket.tokens = Math.min(capacity, bucket.tokens + elapsed * rate);
      bucket.last = now;
      if (bucket.tokens >= 1) {
        bucket.tokens -= 1;
        return true;
      }
      return false;
    }

    Cette fonction recharge selon le temps écoulé, plafonne le seau pour que les jetons ne s’accumulent pas indéfiniment, et dépense un jeton par requête autorisée. C’est peut-être quinze lignes et cela couvre la grande majorité des besoins réels.

    Identifiez correctement le client

    La limitation de débit ne vaut que ce que vaut votre idée de ce qu’est un client. Pour le trafic authentifié, indexez la limite sur la clé d’API ou l’identifiant utilisateur, ce qui est fiable. Pour le trafic anonyme, vous vous rabattez sur l’adresse IP, qui est imparfaite car des utilisateurs derrière le même réseau partagent une IP et les proxys peuvent l’usurper. Si vous êtes derrière un proxy ou un CDN, lisez l’en-tête transmis, mais ne lui faites confiance que lorsque la requête est vraiment passée par votre infrastructure. Mal choisir la clé revient soit à punir des utilisateurs innocents, soit à ne pas arrêter les abuseurs.

    Répondez de la bonne façon

    Quand vous rejetez une requête, faites-le avec le bon statut HTTP et des en-têtes utiles pour que les clients bien élevés s’adaptent. Le statut est 429 Too Many Requests. Incluez toujours un en-tête Retry-After indiquant au client combien de temps attendre, et les en-têtes standard de limitation pour qu’il s’auto-régule avant de heurter le mur :

    HTTP/1.1 429 Too Many Requests
    Retry-After: 30
    RateLimit-Limit: 100
    RateLimit-Remaining: 0
    RateLimit-Reset: 30
    Content-Type: application/json
    
    { "error": "limite de debit depassee, reessayez dans 30 secondes" }

    Un bon client lit ces en-têtes et lève le pied poliment. Un mauvais les ignore, mais au moins vous lui avez laissé sa chance, et vous avez une trace propre de la raison de votre refus.

    Où stocker les compteurs

    Un compteur en mémoire fonctionne pour un seul serveur et s’effondre dès que vous passez à deux, car chaque instance a sa propre vue. Pour quoi que ce soit de distribué, il vous faut un état partagé. Redis est le choix classique ; il est rapide et possède des opérations d’incrément atomiques qui rendent cela facile. À la périphérie, un magasin clé-valeur comme le KV de Cloudflare ou les Durable Objects fait le même travail près de l’utilisateur. Quoi que vous choisissiez, la mise à jour du compteur doit être atomique, sinon deux requêtes simultanées peuvent lire le même compte et passer toutes les deux.

    Combinez-la avec d’autres défenses

    La limitation de débit est une couche, pas tout le mur. Associez-la à l’authentification, à la validation des entrées et à des délais d’attente raisonnables. Placez-la le plus tôt possible dans le cycle de vie de la requête, idéalement avant tout travail coûteux, pour qu’un flot de requêtes bloquées ne vous coûte presque rien. Si vous déployez ce genre de service vous-même, la même plateforme de périphérie que je décris dans le déploiement sur Cloudflare Pages peut faire tourner l’API et son magasin de limitation ensemble, et vous pouvez la publier en confiance via un pipeline GitHub Actions. Construisez le limiteur une fois, gardez-le ennuyeux, et il fait tranquillement son travail sous charge.

  • Guide de conception d’API REST

    Guide de conception d’API REST

    Une API est une promesse. Des qu’un client en depend, chaque bizarrerie que vous avez livree devient permanente, parce que quelqu’un quelque part a ecrit du code contre cette bizarrerie. J’ai maintenu des API pendant des annees et celles qui ont bien vieilli partageaient le meme trait : elles etaient ennuyeuses et previsibles. Voici comment j’y arrive.

    Modelisez les ressources en noms, prenez les verbes a HTTP

    L’URL devrait nommer une chose. La methode dit ce que vous lui faites. Je vois sans cesse des points d’entree comme /getUser et /createOrderNow, et ils combattent tout l’interet de HTTP. Une conception propre utilise des noms au pluriel pour les collections et laisse la methode porter l’action.

    GET    /orders          liste les commandes
    POST   /orders          cree une commande
    GET    /orders/42       recupere une commande
    PATCH  /orders/42       met a jour des champs
    DELETE /orders/42       la supprime
    
    GET    /orders/42/items ressource imbriquee

    PATCH pour les mises a jour partielles et PUT pour le remplacement complet est une distinction a garder. La plupart des vraies mises a jour touchent quelques champs, donc PATCH est ce que je prends, et PUT devient le cas rare ou le client possede vraiment toute la representation.

    Utilisez les codes de statut comme les clients l’attendent

    Renvoyez le code de statut qui correspond a la realite. Un 200 sur une requete echouee parce qu’on « a mis l’erreur dans le corps » casse tout client generique et tout outil de supervision qui lit la ligne de statut. L’ensemble que j’utilise couvre presque tout :

    • 200 pour une lecture ou une mise a jour reussie, 201 quand vous avez cree quelque chose.
    • 400 pour une entree malformee, 422 quand l’entree est bien formee mais semantiquement invalide.
    • 401 quand vous ne savez pas qui ils sont, 403 quand vous le savez et qu’ils n’ont pas le droit.
    • 404 pour une ressource manquante, 409 pour un conflit comme un doublon.
    • 500 uniquement pour de vraies pannes serveur, jamais pour une erreur du client.

    Rendez les erreurs lisibles par la machine

    Un corps d’erreur devrait aider le code appelant a reagir, pas juste afficher une chaine. Je renvoie un code machine stable a cote d’un message humain, pour que les clients puissent brancher sur le code sans analyser une prose que je pourrais reformuler plus tard.

    {
      "error": {
        "code": "insufficient_funds",
        "message": "La carte a ete refusee.",
        "field": "payment_method"
      }
    }

    La coherence compte ici plus que l’ingeniosite. Chaque erreur de l’API devrait avoir la meme forme, pour qu’un client ecrive un seul gestionnaire d’erreur au lieu de dix. C’est le meme instinct que j’apporte aux logs, dont j’ai parle dans les bonnes pratiques d’observabilite : la structure vaut mieux que la prose quand autre chose doit la lire.

    Prevoyez le versionnage avant d’en avoir besoin

    Vous devrez faire un changement cassant tot ou tard. Decidez comment avant de livrer la v1. Je mets la version dans le chemin, /v1/orders, parce qu’elle est visible, cacheable et triviale a router. Le versionnage par en-tete est plus elegant sur le papier et plus penible en pratique quand quelqu’un debogue avec curl. Quel que soit votre choix, la regle est de ne jamais casser une version existante. Les ajouts comme de nouveaux champs optionnels vont bien. Retirer un champ ou changer son type est une nouvelle version.

    Paginez et filtrez des le premier jour

    Toute collection qui peut grandir grandira, et un GET qui renvoie dix mille lignes finira par expirer et emporter une base de donnees avec lui. J’ajoute la pagination a chaque point d’entree de liste des le depart, meme quand les donnees sont minuscules, parce que l’ajouter plus tard est un changement cassant. La pagination par curseur gere mieux les grands jeux de donnees mouvants que l’offset, qui derive quand des lignes sont inserees en cours de parcours.

    • Renvoyez un curseur stable et un signal clair de « il y en a plus ».
    • Autorisez le filtrage par parametres de requete, et documentez exactement quels champs sont filtrables.
    • Plafonnez la taille de page cote serveur pour qu’un client ne puisse pas tout demander d’un coup.

    Soyez strict sur ce que vous acceptez, genereux sur ce que vous renvoyez

    Validez l’entree durement a la frontiere et rejetez tout ce qui est malforme avec un 400 ou 422 clair. Plus vous attrapez les mauvaises donnees pres du bord, moins elles peuvent corrompre l’aval, ce qui ramene directement aux contraintes sur lesquelles je m’appuie dans les bonnes pratiques de schema de base de donnees. Cote sortie, gardez les reponses stables et previsibles pour que les clients puissent faire confiance a la forme. Une API stricte a la porte et coherente a la sortie est une API qu’on aime construire, et cette bonne volonte est ce qui fait adopter votre plateforme.