Une API livrée, testée, déployée. Les routes répondent, la base est branchée, le front affiche ses données. Trois semaines plus tard, un développeur d'une autre équipe branche son client dessus et pose sept questions par Slack en deux jours.
Aucune de ces questions ne porte sur un bug. Elles portent sur le contrat : pourquoi cette route renvoie un 200 avec un message d'erreur dans le corps, pourquoi la liste des commandes s'appelle /orderList alors que celle des produits s'appelle /products, ce qui se passe si le paiement est renvoyé deux fois après un timeout.
Une API est lue avant d'être appelée. Un développeur qui la découvre construit un modèle mental à partir de la première route qu'il voit, puis l'applique aux suivantes. Si les routes suivantes contredisent ce modèle, il retourne dans la documentation à chaque appel, et si la documentation ne tranche pas, il vient te voir.
Ce qui suit couvre les décisions à prendre avant la première ligne de code, avec les conventions qui font qu'une API se devine. Les exemples sont écrits en HTTP brut, sans framework, parce que ces choix se posent à l'identique en Python, en PHP, en Java ou en Node.
REST, RESTful, et la question qui revient en formation
Sur mes sessions backend, la même question tombe à peu près à chaque promotion : quelle est la différence entre REST et RESTful. Beaucoup imaginent deux niveaux de rigueur, une norme d'un côté et une version allégée de l'autre.
Il n'y a pas de différence technique entre les deux mots. REST est le nom d'un style d'architecture décrit par Roy Fielding dans sa thèse de doctorat soutenue en 2000. RESTful est l'adjectif qui qualifie une API construite selon ce style. Le second mot ne désigne aucun standard supplémentaire, aucun label, aucune certification.
La question intéressante se pose autrement : jusqu'à quel point une API donnée applique le style. C'est ce que décrit le modèle de maturité de Leonard Richardson, avec ses quatre paliers, du point d'entrée unique jusqu'aux liens hypermédias renvoyés dans les réponses. La plupart des APIs professionnelles s'arrêtent au palier où les ressources sont bien séparées et où les méthodes HTTP portent l'intention. Mon avis de formateur, assumé : viser le palier hypermédia sur une API interne consommée par deux clients maison ajoute du travail pour un bénéfice que personne dans l'équipe n'ira chercher.
L'autre observation, celle qui coûte le plus cher en pratique, porte sur les URLs. Les conventions ne sont pas appliquées. Voici le genre de routes que je relève dans les projets d'apprenants :
POST /getUser
GET /user_list
POST /deleteProduct?id=12
GET /api/getAllOrdersByUser/42
Chacune de ces routes fonctionne. Prises ensemble, elles obligent le consommateur à apprendre par cœur quatre conventions différentes, et elles rendent tout outillage automatique inutilisable, à commencer par la génération de documentation. Le verbe HTTP est déjà dans la requête, le répéter dans le chemin le contredit à la première évolution : que se passe-t-il le jour où /getUser doit accepter une mise à jour.
Le principe : ressources, sûreté, idempotence
Une API REST expose des choses, pas des actions. Ces choses sont les ressources : un utilisateur, une commande, un produit, une facture. Le chemin identifie la ressource, la méthode HTTP dit ce qu'on veut en faire. Cette séparation est le socle de tout le reste, et elle explique pourquoi les noms de ressources sont des substantifs au pluriel.
Deux propriétés des méthodes HTTP sont définies par la RFC 9110, publiée en juin 2022, et la plupart des cours d'introduction les sautent alors qu'elles décident du comportement en production.
Une méthode est dite sûre quand elle ne modifie rien côté serveur. Un navigateur, un proxy ou un moteur d'indexation se sent autorisé à la rejouer sans prévenir. Une méthode est dite idempotente quand la rejouer plusieurs fois produit le même état final qu'un seul appel. Toute méthode sûre est idempotente, l'inverse est faux : DELETE modifie l'état mais peut être répété sans dégât supplémentaire.
Ce tableau se lit comme une liste de contraintes à respecter. Si ton GET incrémente un compteur en base, il n'est plus sûr, et le premier proxy qui met la réponse en cache faussera tes statistiques. Si ton PUT ajoute une ligne à chaque appel au lieu de remplacer la ressource, il n'est plus idempotent, et un client qui réessaie après une coupure réseau créera des doublons. Pour le socle de lecture d'une requête et de ses en-têtes, l'article sur ce qui passe entre le navigateur et le serveur reprend l'échange ligne par ligne.
Six décisions, avec les échanges HTTP correspondants
Nommer les ressources
Substantifs au pluriel, en minuscules, mots séparés par des tirets. Une collection, puis un identifiant, puis éventuellement une sous-ressource. Les critères de tri et de filtre passent en paramètres de requête, jamais dans le chemin.
GET /users/42
GET /users/42/orders
DELETE /products/12
GET /products?category=outils&sort=price&order=asc
POST /purchase-orders
Une hiérarchie de plus de deux niveaux devient pénible à maintenir. /users/42/orders/8821/items/3/discounts se remplace avantageusement par un accès direct à la ressource finale, avec un filtre si nécessaire.
Choisir le code de réponse
Une création renvoie 201 et indique où trouver la ressource créée. L'en-tête Location est la partie que la plupart des implémentations oublient.
POST /orders HTTP/1.1
Content-Type: application/json
{ "product_id": 12, "quantity": 2 }
HTTP/1.1 201 Created
Location: /orders/8821
Content-Type: application/json
{ "id": 8821, "status": "pending" }
Quelques codes restent sous-utilisés alors qu'ils portent une information précise. Le 202 annonce un traitement accepté qui se terminera plus tard, cas typique d'un export lancé en tâche de fond. Un conflit avec l'état actuel de la ressource appelle un 409, par exemple sur une commande déjà annulée qu'on tente d'annuler à nouveau. Quant au 422, défini dans la RFC 9110 après avoir longtemps vécu dans la spécification WebDAV, il couvre les requêtes syntaxiquement correctes dont le contenu ne passe pas les règles métier, ce qui le distingue du 400 réservé aux requêtes mal formées.
Normaliser le format d'erreur
Chaque équipe invente sa structure d'erreur, et chaque client écrit un parseur différent pour chaque API. La RFC 9457, publiée en juillet 2023 et qui rend obsolète la RFC 7807 de 2016, définit un format commun avec son propre type de média.
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{
"type": "https://api.exemple.fr/problems/validation",
"title": "Champs invalides",
"status": 422,
"detail": "Le champ quantity doit etre superieur a 0.",
"instance": "/orders",
"errors": [
{ "field": "quantity", "message": "doit etre superieur a 0" }
]
}
Les cinq premiers champs sont ceux de la spécification. Le tableau errors est une extension libre, autorisée par la RFC. Si tu croises encore la mention 7807 dans un article ou une bibliothèque, la structure reste compatible, seule la référence a changé.
Rendre un POST rejouable
Un client envoie un paiement, le réseau tombe avant la réponse. Le client ne sait pas si le serveur a traité la demande. S'il réessaie, il risque un double débit, et s'il abandonne, il risque un paiement perdu. La solution répandue consiste à faire porter au client une clé unique par tentative logique, que le serveur stocke pour reconnaître le rejeu.
POST /payments HTTP/1.1
Idempotency-Key: "8e03978e-40d5-43e8-bc93-6894a57f9324"
Content-Type: application/json
{ "amount": 4900, "currency": "EUR" }
Au second appel avec la même clé, le serveur renvoie la réponse déjà produite sans rejouer le traitement. Précision utile au moment où ces lignes sont écrites : cet en-tête fait l'objet d'un brouillon au sein du groupe de travail HTTPAPI de l'IETF, dont la version 07 date d'octobre 2025, et il n'est pas encore publié comme RFC. C'est un standard de fait, popularisé par les plateformes de paiement, pas une norme opposable. Vérifie la documentation de chaque service que tu intègres, la durée de rétention de la clé et le comportement en cas de payload différent varient d'un fournisseur à l'autre.
Paginer sans faire souffrir la base
La pagination par numéro de page se code en dix minutes et tient jusqu'au jour où quelqu'un demande la page 2000. La base doit alors parcourir et jeter 39 980 lignes avant de renvoyer les vingt qui t'intéressent. La pagination par curseur transmet un pointeur opaque vers le dernier élément lu, ce qui transforme le parcours en simple comparaison sur un index.
GET /orders?limit=20&cursor=b3JkZXI6ODgyMQ
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": [ ... ],
"next_cursor": "b3JkZXI6ODg0MQ"
}
Le curseur règle aussi un défaut moins visible de la pagination par offset : quand des éléments sont insérés pendant que le client parcourt les pages, certains résultats apparaissent deux fois et d'autres sont sautés.
Versionner et déprécier
Ajouter un champ dans une réponse ne casse rien, un client correctement écrit ignore ce qu'il ne connaît pas. Renommer un champ, changer son type, rendre un paramètre obligatoire ou supprimer une route casse le contrat. Le versioning par URL, sous la forme /api/v1/users, reste le plus lisible et le plus simple à déboguer, au prix d'une entorse théorique au principe selon lequel une URL identifie une ressource. Le versioning par en-tête est plus propre sémantiquement et complique le cache. Cette comparaison est reprise dans le contexte PHP dans notre guide API Platform avec Symfony 7.
Le vrai travail se situe après. Une version qui disparaît sans préavis fait tomber les clients. Annonce la dépréciation dans la documentation, garde les deux versions en parallèle pendant une période annoncée, mesure le trafic résiduel sur l'ancienne avant de la couper.
Les pièges qui reviennent en revue de code
Renvoyer 200 sur un échec
Un statut 200 accompagné d'un champ success: false oblige chaque client à lire le corps pour savoir si l'appel a réussi. Le code de statut existe pour ça, et toute la chaîne, des proxys aux outils de supervision, s'appuie dessus.
Exposer le schéma de base de données tel quel
Sérialiser directement une entité expose les colonnes techniques, les clés étrangères et parfois un mot de passe haché. Une ressource d'API et une table sont deux modèles distincts, et le second doit pouvoir changer sans casser le premier. Cette séparation rejoint le principe de moindre privilège appliqué aux données.
Ignorer le cache HTTP
Un en-tête ETag sur les réponses, et un client qui renvoie If-None-Match, permettent au serveur de répondre 304 sans reconstruire ni retransmettre la ressource. Sur une liste consultée en boucle par une application mobile, l'économie de bande passante est immédiate.
Confondre OAuth 2.0 et JWT
Les deux ne sont pas des options équivalentes entre lesquelles choisir. OAuth 2.0 est un cadre de délégation d'autorisation, JWT est un format de jeton signé. Un flux OAuth 2.0 peut émettre des JWT, et une API peut utiliser des JWT sans OAuth. Les affiches de bonnes pratiques qui circulent sur les réseaux les listent souvent comme des alternatives.
Ouvrir CORS en grand pour faire passer un appel
Une erreur CORS bloque un développement, quelqu'un met une étoile dans l'origine autorisée, l'appel passe, la configuration part en production. Liste les origines réellement attendues, y compris en environnement de test.
Un dernier point qui n'est pas un piège mais une habitude à prendre : publie une description OpenAPI de ton API, générée depuis le code plutôt qu'écrite à la main. Une documentation séparée du code diverge en quelques semaines. La plupart des frameworks modernes la produisent sans configuration particulière, comme on le voit dans le guide sur une première API REST avec Python et FastAPI.
Ces conventions s'ancrent en les appliquant sur un projet à toi plutôt qu'en les relisant. Si tu cherches un support d'entraînement, la liste de projets backend pour débutants donne des cas assez complets pour rencontrer la question du 409 et celle de la pagination. Côté formation, ces choix de conception sont travaillés en atelier dans le parcours Créer une API REST avec Python et FastAPI et dans Spring Boot : développer des API REST en Java.
Questions fréquentes
Faut-il nommer les collections au singulier ou au pluriel ?
/users désigne l'ensemble, /users/42 un élément de cet ensemble, et la lecture reste cohérente quand on ajoute des sous-ressources. Aucune spécification n'impose ce choix, la cohérence sur l'ensemble de l'API compte davantage que l'option retenue. Mélanger les deux dans la même API est ce qui pose problème.PUT ou PATCH pour une mise à jour ?
HATEOAS est-il obligatoire pour dire qu'une API est REST ?
Quelle différence entre 401, 403 et 404 sur une ressource protégée ?
REST ou GraphQL pour un nouveau projet ?
Où placer la clé d'API dans la requête ?
Authorization est l'emplacement standard, et une clé dédiée à chaque intégration facilite la révocation ciblée.Comment limiter le nombre d'appels d'un client ?
Retry-After indiquant le délai avant nouvelle tentative. Sans cet en-tête, les clients réessaient en boucle et aggravent la charge qu'on cherchait à contenir. Publie le quota dans la documentation, et expose la consommation courante dans les réponses si tes intégrateurs sont nombreux.Si tu ne dois retenir qu'un geste de ce qui précède : sur une API existante, ouvre la liste complète des routes et vérifie qu'aucune ne contient de verbe. Cette relecture prend cinq minutes et elle révèle en général le reste, parce qu'une route nommée /createOrder renvoie presque toujours un 200 là où un 201 était attendu.
Reste la question que cet article n'a pas tranchée, celle des transactions qui touchent plusieurs ressources en une seule opération. Il n'y a pas de réponse propre en REST, seulement des compromis, et le choix se fait au cas par cas.