Une interface de programmation se juge moins à ses performances qu'au temps qu'il faut à un développeur extérieur pour l'utiliser sans poser de question. Cette lisibilité ne dépend pas de la technologie retenue mais d'une poignée de conventions : la façon dont les ressources sont nommées, les codes renvoyés, le format des erreurs et la manière dont les listes sont découpées. Ces décisions se prennent en une demi journée au démarrage et deviennent très coûteuses à changer une fois que des clients consomment l'interface. Cet article passe en revue celles qui comptent réellement, en signalant au passage les habitudes qui produisent des interfaces pénibles.

Nommer les ressources

Le nommage est la première chose que voit un utilisateur de l'interface et il détermine largement l'impression de cohérence. Les conventions établies sont simples et leur respect évite d'avoir à consulter la documentation pour deviner une adresse. Les principes généraux de ce style d'interface sont rappelés dans notre article sur l'API REST, sa définition et son fonctionnement.

Des noms, pas des verbes

Une adresse désigne une ressource, l'action étant portée par la méthode employée. Une adresse comportant un verbe, du type obtenir un client ou créer une commande, trahit une transposition directe d'appels de fonctions. La convention consiste à nommer la ressource au pluriel et à laisser la méthode exprimer l'intention. Cette règle unique élimine à elle seule une bonne moitié des adresses illisibles rencontrées en pratique. Elle a par ailleurs l'avantage de rendre l'interface devinable : quelqu'un qui connaît une adresse en déduit les autres sans documentation.

Le pluriel systématique

Employer le pluriel pour toutes les collections, y compris lorsque l'on accède à un élément unique par son identifiant, produit une structure prévisible. Alterner singulier et pluriel selon le contexte oblige à mémoriser des exceptions. Cette cohérence vaut plus que le choix lui même : une interface entièrement au singulier serait acceptable, une interface qui mélange les deux ne l'est pas. La prévisibilité est le critère qui prime sur l'élégance. Un développeur qui doit vérifier la documentation pour chaque appel perd un temps considérable, et ce coût se répète à chaque nouvelle intégration.

L'imbrication mesurée

Exprimer une relation par l'imbrication, en plaçant les commandes sous un client, est naturel et devient illisible au delà de deux niveaux. Une adresse comportant quatre segments imbriqués oblige à connaître toute la chaîne pour accéder à un élément que son identifiant suffirait à désigner. La pratique établie consiste à limiter l'imbrication à un niveau et à exposer par ailleurs la ressource en accès direct. Les deux chemins peuvent coexister sans difficulté. Le chemin imbriqué sert alors à lister les éléments liés, le chemin direct servant à lire et modifier un élément identifié.

La casse et les séparateurs

Les segments d'adresse s'écrivent en minuscules, les mots composés étant séparés par un tiret. Le mélange de conventions à l'intérieur d'une même interface, quelques adresses en tiret et d'autres en soulignement, est un défaut fréquent sur les projets où plusieurs développeurs sont intervenus sans convention écrite. Cette règle tient en une ligne dans la documentation technique du projet et elle se vérifie en relecture. Son absence coûte plus cher qu'on ne le croit en support. Chaque incohérence produit tôt ou tard un message d'un intégrateur, et la correction devient impossible une fois l'interface publiée.

Les actions qui ne sont pas des ressources

Certaines opérations ne correspondent à aucune ressource : relancer un traitement, envoyer un rappel, valider un panier. Les forcer dans le modèle produit des contorsions peu lisibles. La pratique courante consiste à les exposer comme sous ressources d'action, clairement identifiées, plutôt que de faire semblant. Un modèle appliqué avec pragmatisme est plus utile qu'un modèle appliqué avec rigueur au prix de la clarté. Ces cas restent rares sur une interface bien découpée, et leur nombre constitue d'ailleurs un bon indicateur de la qualité du découpage.

Le versionnement

Placer le numéro de version dans le chemin est la solution la plus lisible et la plus répandue, même si elle n'est pas la plus élégante conceptuellement. Elle permet de faire coexister deux versions sans ambiguïté et de lire l'appel sans consulter les en têtes. Le versionnement par en tête est plus propre et il complique les tests manuels et les journaux. Sur une interface destinée à des tiers, la lisibilité l'emporte généralement. Le débat entre les deux approches revient régulièrement et il compte nettement moins que le fait d'avoir prévu un versionnement dès le premier jour.

Structure d’une réponse paginée et d’un message d’erreur normalisé

Les codes de retour et les erreurs

Le code renvoyé est la première information exploitée par un client automatisé et son usage approximatif est la source de bogues la plus fréquente côté consommateur. Le protocole en propose suffisamment pour couvrir tous les cas courants, et une petite dizaine suffit en pratique. Les employer correctement demande surtout de résister à la tentation de tout ramener à deux ou trois codes connus.

Le succès en trois cas

Une lecture ou une modification réussie renvoie un code de succès simple. Une création renvoie un code indiquant la création, accompagné de l'adresse de la ressource créée dans un en tête dédié. Une opération réussie sans contenu à renvoyer, typiquement une suppression, emploie le code sans contenu. Ces trois cas couvrent l'immense majorité des réponses positives et leur distinction évite au client de deviner ce qui s'est passé. Renvoyer l'adresse de la ressource créée lui épargne en outre une requête supplémentaire pour la retrouver.

Distinguer les erreurs du client

Une requête mal formée, une validation qui échoue, une ressource inexistante et un accès refusé sont quatre situations différentes qui appellent quatre codes différents. Renvoyer un code générique pour toutes prive le client de la possibilité de réagir correctement. La distinction entre non authentifié et non autorisé est particulièrement importante, la première invitant à se connecter, la seconde signalant que c'est inutile. Confondre les deux conduit les clients à boucler sur des tentatives de reconnexion qui ne changeront rien, comportement pénible à diagnostiquer à distance.

Ne pas renvoyer un succès pour une erreur

Renvoyer systématiquement un code de succès en plaçant l'état réel dans le corps de la réponse est une pratique encore répandue et profondément gênante. Elle oblige chaque client à analyser le corps pour savoir si l'appel a fonctionné, et elle casse tous les mécanismes automatiques de reprise et de surveillance. C'est le défaut qui rend une interface le plus pénible à consommer. Il provient généralement d'un cadre applicatif mal configuré ou d'une couche intermédiaire qui uniformise les réponses, ce qui rend la correction plus simple qu'il n'y paraît.

Un format d'erreur unique

Toutes les erreurs doivent partager la même structure de corps : un identifiant d'erreur stable, un message lisible et, le cas échéant, le détail par champ. L'identifiant permet au client de réagir sans analyser le message, qui peut être traduit ou reformulé sans casser personne. Cette séparation entre code machine et texte humain est le point le plus souvent négligé. Elle se décide une fois et vaut pour toute la durée de vie de l'interface. Publier la liste des identifiants d'erreur dans la documentation permet aux clients de traiter chaque cas de façon explicite.

Les erreurs de validation détaillées

Une validation qui échoue sur trois champs doit renvoyer les trois, pas seulement le premier. Un client qui doit soumettre quatre fois de suite pour découvrir successivement chaque problème offre une expérience détestable à ses propres utilisateurs. Le format habituel associe à chaque champ concerné un code et un message. Ce détail supplémentaire coûte peu à produire et il change complètement l'usage. La plupart des bibliothèques de validation renvoient déjà l'ensemble des erreurs, il suffit de ne pas s'arrêter à la première.

Ne rien divulguer d'inutile

Un message d'erreur ne doit contenir ni trace d'exécution, ni requête de base de données, ni chemin de fichier. Ces informations aident un attaquant et n'aident pas le client légitime. La trace complète appartient aux journaux du serveur, avec un identifiant de corrélation renvoyé au client pour permettre le rapprochement lors d'un signalement. Cette pratique règle les deux besoins sans compromis, et elle rejoint les questions traitées dans notre article sur la manière de journaliser une API sans stocker de données personnelles.

Situation Code Corps attendu
Lecture réussie Succès simple La ressource
Création réussie Créé La ressource et son adresse
Suppression réussie Sans contenu Vide
Requête mal formée Requête incorrecte Erreur normalisée
Non authentifié Non autorisé Erreur normalisée
Authentifié sans droit Interdit Erreur normalisée
Ressource inexistante Introuvable Erreur normalisée
Validation échouée Entité non traitable Détail par champ
Quota dépassé Trop de requêtes Délai avant nouvelle tentative

Pagination, tri et filtres

Toute collection finit par devenir trop grande pour être renvoyée entière, et la stratégie de découpage doit être décidée avant la mise en service. Changer de mécanisme après coup casse tous les clients existants, ce qui en fait l'une des décisions les plus engageantes de la conception. Mieux vaut donc trancher en fonction de ce que la collection deviendra dans trois ans que de sa taille actuelle. Elle rejoint directement la question du contrôle de charge que nous traitons dans notre article sur la façon de limiter le débit d'une API pour éviter les abus.

Pagination par position

Le découpage par numéro de page et taille de page est le plus simple à comprendre et à implanter. Il convient parfaitement aux jeux de données stables et de taille modérée. Sa limite est connue : sur une collection qui bouge pendant le parcours, des éléments peuvent être vus deux fois ou manqués. Sur un catalogue mis à jour quotidiennement, cette limite est théorique ; sur un flux d'événements, elle est bloquante. Le choix se fait donc collection par collection plutôt que pour l'interface entière, rien n'interdisant de mêler les deux mécanismes.

Pagination par curseur

Le curseur, jeton opaque désignant une position dans un ordre stable, règle le problème précédent et coûte plus cher à implanter. Il interdit d'accéder directement à la page quarante deux, ce qui est rarement un besoin réel. Sur les collections volumineuses ou en évolution constante, c'est la seule approche satisfaisante. Le jeton doit rester opaque pour le client, faute de quoi il finira par être construit à la main. Encoder son contenu suffit à décourager cette pratique, qui rendrait ensuite toute évolution impossible.

Toujours renvoyer les métadonnées

Une réponse paginée doit indiquer comment obtenir la suite, par une adresse complète plutôt que par des paramètres à recomposer. Y ajouter le nombre total d'éléments est utile lorsque le calcul n'est pas coûteux, et il peut l'être sur de très grandes tables. Dans ce cas, indiquer simplement s'il existe une page suivante suffit. Renvoyer un total approximatif sans le signaler est en revanche une mauvaise idée. Les clients l'affichent tel quel à leurs propres utilisateurs, qui constatent l'écart et le signalent.

Le tri explicite

Un ordre par défaut doit exister et être documenté, sans quoi la pagination devient instable dès la deuxième page. Cet ordre doit être total, c'est à dire déterministe même en cas d'égalité sur le critère principal, ce qui impose d'ajouter l'identifiant comme critère secondaire. Cette subtilité est la cause d'une classe entière d'anomalies difficiles à reproduire. Le paramètre de tri exposé au client doit se limiter aux champs réellement indexés. Autoriser un tri sur un champ non indexé revient à laisser n'importe qui déclencher une requête coûteuse sur la base.

Des filtres bornés

Exposer un mécanisme de filtrage générique, permettant d'interroger n'importe quel champ avec n'importe quel opérateur, paraît élégant et devient vite ingérable en performance comme en sécurité. Il vaut mieux exposer une liste fermée de filtres correspondant aux usages réels, quitte à l'élargir sur demande. Cette approche laisse le contrôle sur les index et sur le coût des requêtes. Elle simplifie aussi considérablement la documentation. Une liste de dix filtres nommés se lit en trente secondes, là où une grammaire de requête générique demande une page entière d'explications.

Limiter la taille de page

Un plafond doit être imposé, faute de quoi un client demandera un jour cent mille éléments et fera tomber le service. La valeur par défaut se situe couramment entre vingt et cinquante, le plafond entre cent et quelques centaines selon le poids des objets. Renvoyer une erreur explicite lorsque la demande dépasse le plafond est préférable à un ajustement silencieux, qui laisse le client croire qu'il a tout reçu. Ce silence produit des intégrations qui semblent fonctionner et perdent discrètement une partie des données.

Difficultés signalées par les développeurs lors d'une intégration d'API
Format d'erreur incohérent
29 %
Documentation divergente du code
24 %
Pagination instable ou absente
19 %
Codes de retour mal employés
17 %
Absence d'environnement d'essai
11 %

Motifs de blocage relevés lors d'intégrations menées avec des interfaces développées sur mesure.

Ce qui rend une API agréable à vivre

Au delà des conventions, quelques décisions font la différence entre une interface correcte et une interface que les développeurs apprécient réellement.

La cohérence des formats

Les dates doivent employer un format unique et normalisé, avec le fuseau explicite. Les montants doivent être exprimés dans une unité déclarée, en centimes ou avec une devise associée, jamais en nombre flottant sans précision. Les identifiants doivent avoir un type constant sur toute l'interface. Ces trois règles éliminent la majorité des incompréhensions constatées à l'intégration. Elles évitent aussi les erreurs d'arrondi sur les montants, catégorie de bogue particulièrement désagréable à corriger après coup.

Les champs stables

Un champ renvoyé une fois doit continuer à l'être, et sa disparition constitue une rupture de compatibilité même si la documentation ne le mentionnait pas. Ajouter des champs est en revanche sans danger, à condition que les clients ignorent ce qu'ils ne connaissent pas. Cette asymétrie doit être comprise par toute l'équipe, car elle détermine ce qui peut être livré sans prévenir. Écrire cette règle dans les conventions du projet évite qu'un développeur ne retire un champ devenu inutile de son point de vue.

L'idempotence des écritures

Une requête de création rejouée après une coupure réseau ne doit pas produire deux enregistrements. Accepter une clé d'idempotence fournie par le client, et renvoyer la réponse initiale si la même clé revient, règle ce problème de façon élégante. C'est un ajout modeste qui évite des incidents coûteux sur les opérations financières. Peu d'interfaces internes le prévoient et toutes finissent par en avoir besoin. La conservation des clés pendant quelques heures suffit à couvrir les cas de reprise, ce qui limite le coût de stockage.

Une documentation générée

Une spécification écrite à la main diverge du code en quelques semaines. La générer depuis les définitions du code, ou générer le code depuis la spécification, garantit la cohérence. Le format retenu importe peu, l'essentiel étant qu'un développeur puisse essayer un appel depuis la documentation. Cette possibilité d'essai immédiat réduit considérablement le volume de questions. Elle sert aussi à l'équipe interne, qui dispose ainsi d'un moyen rapide de vérifier un comportement sans écrire de code.

Un environnement d'essai

Disposer d'un environnement séparé, avec des jeux de données réalistes et des identifiants de test faciles à obtenir, change complètement l'expérience d'intégration. Sans lui, les partenaires testent en production ou renoncent. C'est probablement l'investissement au meilleur rapport pour une interface destinée à des tiers. Un jeu de données couvrant les cas limites, commande annulée, client sans adresse, produit épuisé, vaut mieux qu'un catalogue volumineux et uniforme.

Prévoir la dépréciation

Une interface vivante finit par devoir retirer des champs ou des adresses. Annoncer la dépréciation par un en tête dédié, avec une date, et prévenir les consommateurs identifiés, transforme une rupture brutale en transition maîtrisée. Ce processus s'écrit une fois et sert pendant toute la vie de l'interface. Son absence explique pourquoi tant d'équipes n'osent plus rien changer à leur propre interface.