Ce document a pour vocation de recenser les lignes directrices et bonnes pratiques de Niji s'agissant de la conception d'APIs RESTful pragmatiques.
Nouveau venu dans le monde des APIs ? → An Introduction to API’s
Préambule :
Aucun standard absolu n'existe réellement concernant la façon de concevoir une API web moderne.
Néanmoins de nombreuses ressources plus ou moins riches sont disponibles en ligne sous la forme de white papers ou simples articles qui visent à faire un retour d'expérience ou des préconisations de la part d'acteurs souvent légitimes.
Faire le tri parmi la foultitude de ressources disponibles est une tâche ardue; c'est pourquoi il est apparu nécessaire pour Niji de partager avec l'ensemble des forces vives concernées par le sujet un corpus minimum établissant et décrivant les règles essentielles à suivre pour obtenir un design API efficient et pragmatique.
Se doter d'API efficientes est devenu un objectif vital pour fluidifier l'intégration des services des SI, et donc pour la transformation numérique de nos clients.
Liminaire :
Dans la suite de ce document, aucune distinction n'est faite entre une API ayant vocation à être publiée publiquement et une API exclusivement interne, afin d'éviter de sacrifier des bonnes pratiques pour des mauvaises raisons (syndrome de l'intranet).
Pour l'ensemble des pratiques et préconisations recensées on distinguera ce qui est "non arbitrable" de ce qui est "dépendant du contexte" afin de ne pas sombrer dans la sur-qualité tout en garantissant un minimum d’interopérabilité.
Signalétique | Signification |
doit | doit être respecté, non arbitrable |
devrait | devrait être respecté si le sujet est concerné |
option | optionnel, proposition de bonne pratique qui peut faire débat |
Cela signifie aussi qu'un standard est suivi s'il apporte une valeur, il n'est pas adopté "a priori".
Postulats
Les recommandations énoncées dans ce document sont basées sur les principes suivants :
- Les standards du web sont suivis lorsqu'ils font sens
- Les ressources exposées sont simples à comprendre pour un développeur et explorables avec une barre d'adresse de navigateur
- Dans la mesure du possible le contact avec l'API doit être agréable, simple et consistant
- L'API doit être suffisamment flexible pour pouvoir subvenir à tous les besoins des consommateurs (applis web, applis mobiles, objets connectés)
- L'API ne doit pas préjuger de l'usage, mais le servir; l'API représente le SI, c'est-à-dire les processus métiers qui sont exposés au monde
Convention de nommage
doit
Appliquer comme convention de nommage :
- lowerCamelCase ou snake_case pour les contenus (attributs JSON)
- kebab-case pour les contenants (URLs)
Les contraintes techniques des frameworks ou stacks mis en oeuvre sont souvent utilisées à tort comme argument pour décider de choisir la convention snake_case plutôt que kebab-case.
Bien que ce débat ne soit pas véritablement tranché, le fait est que la majorité des solutions de blogs (sensibles par définition aux optimisations SEO et aux standards du web) ont opté pour kebab-case (appelé aussi "hyphen-separated-case").
doit
Les noms d'attributs doivent toujours respecter un encodage ASCII, avec comme premier caractère une lettre, $, ou _ (pas de chiffre), chaque caractère suivant peut être une lettre, $, _ ou un chiffre.
URLs et actions
Les URLs et actions d'une API représentent la carte qui permet de naviguer dans le SI.
Une API est organisée en ressources logiques localisées par leur URI et manipulées par un verbe, verbe représenté par une méthode HTTP (GET, POST, PUT, PATCH, DELETE).
Ces principes forts du style d'architecture REST ont été introduits par Roy Fielding dans sa thèse sur les architectures logicielles basées sur les réseaux, plus précisément au chapitre 5.
doit
Choisir la bonne méthode pour la bonne action :
Méthode | Action | Situation | Remarques |
GET | lister, obtenir | obtenir la représentation d'une ressource | le SI n'est pas modifié (idempotence) |
POST | ajouter | ajouter un élément dans une collection de ressources | le SI est modifié et souvent un identifiant est généré pour la nouvelle ressource créée (et communiqué via l'entête Location) |
PUT | modifier complètement | remplacer une ressource | le payload soumis est censé représenter complètement la ressource, c'est-à-dire que la ressource peut être écrasée par les données envoyées |
PATCH | modifier partiellement | mettre à jour une ressource partiellement | les données composantes de la ressource existante et non présentes dans le payload restent inchangées |
DELETE | supprimer, purger | supprimer, détruire une ressource, ou vider une collection de ressources | classiquement la réponse ne contient pas de corps et le statut est 204 |
HEAD | vérifier | vérifier l'obtention d'une ressource, obtenir des méta données | aucun corps / aucune représentation n'est retournée dans la réponse; rarement utilisée, cette méthode peut servir à vérifier la localisation d'une ressource |
OPTIONS | se renseigner | obtenir des informations sur les modalités de la communication | permet au client d'en savoir plus sur les prérequis d'une ressource; très rarement implémenté |
Il est à noter que la méthode POST est parfois utilisée comme méthode de remplacement de PUT, PATCH ou DELETE, dans des situations d'exceptions comme un client ne supportant pas toutes les méthodes (navigateur obsolète).
Dans une telle situation, on utilise la méthode POST en précisant la "vraie" méthode dans un entête "X-HTTP-Method-Override" ou un paramètre de requête "_method"; la plupart des frameworks pour applications web sont censés supporter ce fallback.
Le fait qu'un service web propose cette souplesse de fonctionnement ne l'affranchit absolument pas de respecter le standard, les deux modes n'étant pas exclusifs.
Nommage d'une ressource (URI)
doit
Une ressource doit représenter un nom commun ("noun"), de préférence en anglais car la localisation d'une ressource ne doit pas préjuger de la langue de préférence du consommateur (l'anglais étant le choix le plus générique possible).
La sémantique est la chose la plus importante à avoir en tête pour obtenir un bon nommage.
Exemples :
Ressource | Nom |
Une pizza | pizza |
Un client | customer |
Le client 3 | customer?id=3 |
Le client 3 | customers/3 |
L'adresse du client 4 | customers/4/address |
L'email 3 de l'utilisateur 2 | users/2/emails/3 |
Dans certaines situations exceptionnelles, où il paraît délicat de respecter ce principe, on utilise une forme RPC, néanmoins cela reste une enfreinte aux bonnes pratiques.
Concrètement lorsque l'on est résigné à choisir un style RPC, on utilise un verbe plutôt qu'un nom commun; le verbe désignant explicitement une action.
Exemples RPC :
Ressource | Nom |
S'enregistrer | signup |
Se connecter | signin |
Supprimer une page des favoris | pages/4/unstar |
Sémantique
Pour nommer efficacement une ressource, utiliser des moyens mnémotechniques simples en prononçant mentalement des phrases adaptées au besoin.
Exemples :
Besoin | Traduction sémantique | Nommage résultant |
Obtenir l'adresse de l'utilisateur 7 | Obtenir l'adresse de l'élément 7 de la collection des utilisateurs | GET /customers/7/address |
Obtenir la facture de mon compte | ... | GET /account/invoice |
Créer un client | Poster un élément dans la collection des clients | POST /customers |
Conseil : consacrez tout le temps nécessaire à la définition d'une bonne sémantique, vous ne le regretterez pas.
Pluriel
Dans la plupart des cas, les ressources à concevoir désignent une collection permettant d'ajouter ou de supprimer des éléments.
La collection désigne alors naturellement un pluriel, tandis que l'élément désigne un singulier.
Néanmoins, un élément peut être vu comme un sous-ensemble d'une collection, sa localisation se résumant alors à un sous-chemin par rapport à la collection (un singulier n'est qu'un item d'un pluriel).
devrait
Afin de rendre les chemins de ressources consistants et intuitifs, on privilégie l'utilisation du pluriel plutôt que le singulier.
Exemple appliqué à CRUD :
Besoin | Ressource |
Obtenir les pizzas | GET /pizzas |
Créer une pizza | POST /pizzas |
Obtenir la pizza carbonara | GET /pizzas/carbonara |
Modifier la pizza carbonara | PUT /pizzas/carbonara |
Supprimer la pizza carbonara | DELETE /pizzas/carbonara |
Par extension, une requête DELETE /pizzas correspondrait à "purger toutes les pizzas" (rarement implémenté)
Codes de statut
doit
Respecter les codes de statut normalisés par HTTP.
Lecture simple de la classification des codes :
- 200 à 299 : toute réponse de confirmation positive (opération effectuée, demande enregistrée, …)
- 400 à 499 : toute réponse indiquant une erreur du client (requête incomplète, mauvais prérequis, ressource introuvable, …)
- 500 à 599 : toute réponse indiquant une erreur du serveur (service indisponible, erreur interne, …)
Principaux codes utilisés (donc à implémenter) :
- 200 : ok avec une réponse
- 201 : ok ressource créée
- 202 : ok mais traitement effectué de manière asynchrone (plus tard)
- 204 : ok sans réponse (pas de donnée à renvoyer)
- 400 : mauvais paramètres dans la requête (query string, ou payload)
- 401 : non autorisé, une authentification est nécessaire
- 403 : accès interdit, droits insuffisants
- 404 : ressource introuvable
- 405 : mauvaise méthode
- 409 : conflit, la ressource existe déjà (utilisé lors d'une création)
- 429 : trop de requêtes (utilisé avec des quotas de limitation d'accès)
- 500 : erreur interne, ne devrait jamais arriver en situation normale (mauvaise odeur indiquant un déficit de gestion d'erreur)
- 503 : service indisponible (par exemple en cas d'opération de maintenance)
Gestion d'erreur
Une erreur est une réponse comme les autres.
De manière à simplifier la gestion des cas d'erreurs par le consommateur, la meilleure approche consiste à :
- Choisir le bon code statut adapté à la situation
- Fournir une réponse (body), le code statut étant la première information à vérifier côté client, elle s'avère souvent insuffisante :
- avec un code applicatif interprétable de manière programmatique (et documenté)
- avec un message à destination de l'utilisateur final (dans l'idéal [I18N](fr.wikipedia.org/wiki/Internationalisation_.. compliant)
- éventuellement des informations supplémentaires sur l'origine ou les conditions de l'erreur, dans un attribut extra
doit
Toutes les erreurs doivent présenter une réponse JSON et respecter le même schéma.
Exemple pour un code statut 400 :
{
"code": "BAD_REQUEST",
"message": "Mauvais format",
"extra": {
"fieldName": "date",
"value": "5/13/9",
"expectedFormat": "[0-9]{2}/[0-9]{2}/[0-9]{4}"
}
}
Documentation
devrait
Une API existe d'abord à partir de sa documentation, il est donc essentiel de soigner la documentation d'une API.
Dans l'idéal donner une dimension "exécutable" à la documentation, et utiliser un format adapté au monde des APIs (comme APIb par exemple)
- Ne pas considérer les erreurs comme des cas à part : les erreurs sont des réponses comme les autres (avec un code statut et un corps)
- Regrouper les ressource par affinité / thématique
- La documentation ne devrait dans l'idéal contenir aucune donnée confidentielle, et donc pourrait être exposée publiquement
- Les exemples illustrant la documentation devrait pouvoir être utilisés tels quels avec n'importe quel client API
- Les changement de contrat / depréciations doivent être clairement mentionnés
Versions
Une API se doit de garantir un maximum de stabilité concernant son contrat de service.
Pour éviter de casser un contrat et provoquer ainsi des erreurs au niveau des consommateurs, il existe deux techniques :
- Profiter des possibilités d'extension du flux JSON en respectant son schéma
- Délivrer une nouvelle version de l'API tout en maintenant l'ancienne pour permettre une migration douce des clients
Conseil : éviter de maintenir trop de versions différentes, limiter autant que possible à deux versions (l'actuelle et la précédente)
doit
Respecter le principe de rétro-compatibilité tel que spécifié par le versioning sémantique.
Il y a différentes façons de négocier, entre le client et le serveur, la version de l'API a consommer :
- URL : la version fait partie d'un préfixe d'URL (exemple : api.niji.fr/masuperapi/v2), pas idéal sémantiquement (la localisation de la ressource devient dépendante de la version) mais très facile à utiliser
- Un entête de requête custom (exemple : API-Version) : pas idéal du point de vue du respect des specs HTTP (réinvention de la roue)
- l'entête de requête Accept : convention du format compliquée, pénible à traiter (application/vnd.masuperapi.v2+json)
- Aucun : toujours considérer la dernière version, avec le risque d'instabilité du contrat
- Mix : supporter l'URL et les entêtes pour, par exemple, pouvoir préciser une version mineure (démarche luxueuse)
option
La solution 1 n'est pas idéale mais est facilement maîtrisée et simple à apréhender, c'est pourquoi elle est le plus souvent privilégiée.
La version exposée par l'API n'est pas nécessairement corrélée avec la version interne du produit, qui généralement respecte les principes de la version sémantique.
option
Pour indiquer aux clients qu'une version est dépréciée, il est bienvenu d'utiliser des entêtes de réponses X-API-Version et X-API-Warn afin de le prévenir.
Recherche, tri et filtrage
Sélection vs inflation : l'API doit fournir un moyen d'éviter l'inflation, c'est-à-dire éviter au consommateur de récupérer des dizaines d'infos alors qu'une seule l'intéresse.
L'objectif est de contribuer aux performances des échanges et soulager les consommateurs qui devront opérer des filtres ou tris sur les données récupérées en les traitant directement dans l'API.
devrait
Pour des ressources de type collection comportant un nombre important d'éléments, il faut favoriser les performances du réseaux et des ressources du client en proposant ce type de paramètres optionnels :
Nom paramètre | Effet | Exemple |
select | permet de ne sélectionner qu'une portion du JSON en appliquant un filtre sur les attributs | ?select=age:35,pays:france |
unselect | le contraire de select, filtre le JSON en ne retenant que les items qui ne matchent pas aux critères | ?unselect=nom:dupont |
sort | trier la collection d'objet sur le critère d'un attribut (préfixe "-" pour un ordre inversé) | ?sort=-departement |
pick | ne conserver que les attributs spécifiés | ?pick=nom,prenom,age |
omit | supprimer les attributs spécifiés | ?omit=adresse,tel |
Il est parfois opportun de proposer des alias pour faciliter la recherche, dans ce cas on utilise généralement la forme d'un adjectif : GET /factures/anciennes
Les fonctionnalités select et unselect peuvent être simplifiées dans le cas de ressources simples, par un simple paramètre de query string représentant le nom de l'attribut à filter.
Par exemple si je souhaite obtenir la liste de toutes les pizzas à 8€ → GET /pizzas?price=8
Le fait d'implémenter systématiquement l'ensemble de ces fonctionnalités est dans la plupart des cas de la sur-qualité (sauf dans cas d'une factorisation portée par un framework).
Ce qui doit primer est : le bon sens, des vrais use-cases, et du pragmatisme.
Réponse utile
doit
Le résultat d'une requête POST, PUT ou PATCH doit retourner une réponse utile.
Le bon contre-exemple est la méthode DELETE, qui le plus souvent ne fait que confirmer que l'opération s'est bien déroulée : code statut 204 (donc sans corps)
Les opérations de création ou mise à jour sont censées retourner un corps représentant la ressource résultante potentiellement différente du payload de la requête.
L'identifiant d'une nouvelle ressource n'est pas censé être prédictible et connu par le client, ainsi l'opération de création doit retourner un entête Location avec l'URI de la ressource créée, permettant ainsi au consommateur de retrouver la ressource.
JSON et multi-représentation
Une approche encore assez répandue consiste à envisager l'utilisation de XML comme format d'échange de données, pour des raisons d'héritage du legacy (approche historique SOA). L'évolution sur le marché de l'utilisation de XML est inversement proportionnelle à JSON (qui dépasse XML depuis 2012).
Choisir XML revient à s'endetter délibérément.
Une autre approche avisée consiste à répondre à des besoins de multi-représentation et permettre au client de choisir lui-même la représentation de la ressource qui lui convient (image, pdf, …).
doit
Dans tous les cas le format par défaut à privilégier est JSON (fallback).
La multi-représentation d'une ressource peut techniquement être négociée de plusieurs façons :
par extension dans l'URL
Exemple :
GET /account/invoice.pdf HTTP/1.1 Host: api.niji.fr
par l'entête Accept
Exemple :
GET /account/invoice HTTP/1.1 Host: api.niji.fr Accept: image/png
D'autres méthodes existent mais sont la plupart du temps à proscrire.
devrait
L'usage de l'entête Accept est à privilégier car plus respectueux du principe de localisation unique d'une ressource.
Schémas JSON
JSON est un format de données qui supporte la notion de schéma.
A l'instar des schémas XML (ou DTDs), les schémas JSON permettent de définir la structure d'un flux JSON ainsi que le format des valeurs d'attributs attendues.
Il existe des outils (validateurs) qui permettent de vérifier la bonne validité des documents JSON, ce qui affranchit de faire des contrôles répétés à chaque niveau de l'architecture logique de l'application.
option
L'utilisation généralisée des schémas JSON présente de nombreux avantages
Pour comprendre les schémas JSON, il suffit de retenir quelques principes simples :
- le schéma se focalise sur la structure du document : c'est-à-dire le nom des attributs, voire le format des valeurs d'attributs, pas les valeurs elles-mêmes
- lorsque le JSON soumis propose plus d'informations que ce qui est attendu, par défaut le schéma le tolère : cette caractéristique est intéressante pour appliquer un principe ouvert/fermé à la modélisation de la structure des flux JSON
Exemple de schéma :
{
"title": "Contact JSON schema",
"type": "object",
"properties": {
"username": { "type": "string", "unique": true },
"email": { "type": "string", "format": "email", "unique": true },
"firstName": { "type": "string" },
"lastName": { "type": "string" },
"extra": { "type": [ "object", "null" ] }
},
"additionalProperties": false,
"required": ["username", "email"]
}
Dans cet exemple, on a une bonne illustration du principe ouvert/fermé :
- on spécifie de manière précise les différents attributs attendus pour un contact
- on exige la présence d'un username et d'un email
- on refuse tout attribut supplémentaire qui ne serait pas spécifié
- on permet la fourniture de données supplémentaires dans l'attribut optionnel "extra"
devrait
Les attributs de type date ou heure devraient respecter l'un des formats spécifiés par la RFC 3339, tandis que les durées devraient respecter la norme ISO 8601.
Enveloppe
Il arrive parfois de rencontrer des réponses JSON enveloppées dans un attribut :
{
"result": {
"firstname": "john",
"lastname": "doe"
}
}
La justification d'une telle approche serait de faciliter l'ajout éventuel, plus tard, de méta données sans affecter la partie "données" :
{
"result": {
"firstname": "john",
"lastname": "doe"
},
"created": "01/02/2016"
}
devrait
Il est plus efficient de privilégier une structure simple répondant directement au besoin, quitte à la faire évoluer (les schémas JSON le supportent généralement) :
{
"firstname": "john",
"lastname": "doe",
"created": "01/02/2016"
}
Le fait d'envelopper les données permet par exemple de répondre aux contraintes de JSONP : c'est un cas particulier.
Le cas où l'on souhaite proposer une pagination est aussi une situation fréquente d'utilisation d'une enveloppe, pour séparer les éléments de pagination des données; là encore il existe aujourd'hui des meilleures façons d'implémenter la pagination et éviter les enveloppes.
devrait
Dans le cas où la réponse est un tableau, les extensions de schémas sont rendues délicates, c'est la bonne situation pour utiliser une enveloppe, de cette manière on a toujours un JSON qui représente un objet (donc extensible).
Associations
doit
Lorsqu'une ressource ne peut exister qu'à travers son association à une autre ressource, on parle alors de "composition" et on applique un principe de hiérarchie à sa localisation.
Exemple : obtenir l'adresse du contact 678
GET /contacts/678/address
Ici l'adresse est considérée comme une partie du contact, qui a sa propre structure mais dont la durée de vie est intimement liée au contact.
devrait
Il est parfois pertinent d'utiliser cette approche pour proposer un alias, alternative pratique pour accéder à une ressource associée qui n'est pas une composition, dans ce cas la localisation par hiérarchie vient en plus de la localisation directe (plusieurs façons d'accéder à une même ressource).
Exemple : obtenir les amis du contact 678
GET /contacts?friend=678
Alias plus élégant :
GET /contacts/678/friends
Dans tous les cas, veillez à ne pas abuser de la notion de hiérarchie dans la localisation de ressources, aller au delà de deux niveaux devrait interroger sur la qualité du modèle.
Entêtes
Les entêtes de requêtes permettent au client de mieux négocier les échanges avec le serveur, tandis que les entêtes de réponses fournissent des méta informations parfois précieuses au client.
devrait
Qu'il s'agisse de la requête du client ou de la réponse du serveur, utiliser en priorité les entêtes déjà existants dans la norme HTTP, avant d'envisager de créer des entêtes custom.
doit
Tout entête custom doit être préfixé par X-
doit
Un entête n'est pas sensible à la casse, respecter cette règle pour garantir la neutralité par rapport aux technologies employées par le client : "ACCEPT" est équivalent à "accept" et "Accept", etc.
Entêtes les plus courants
Entêtes à utiliser lors d'échanges API conformément aux standards HTTP :
Requête :
Entête | Description |
Accept | Type de média accepté par le client pour la réponse (exemple : application/json) |
Accept-Language | Langue acceptée par le client pour la réponse (exemple : da) |
Authorization | Fournit une autorisation (Basic Auth, token OAuth, HMAC, ...) éventuellement requise par l'API |
Content-Length | Indique la longueur en octets du corps de la requête |
Content-Type | Indique le type de média du corps de la requête |
X-Powered-By | Indique le nom du produit qui propulse l'API (exemple : PHP/5.2.6-2ubuntu4.2), il est courant de ne pas définir cet entête pour des raisons de sécurité |
X-Cache | Indique si la réponse est issue d'un cache (HIT) ou non (MISS), à utiliser avec une politique de cache |
X-Forwarded-For | Fournit la liste des IPs des différents noeuds HTTP parcourus entre le client et le serveur, utile au serveur pour obtenir l'IP d'origine du client lorsque sa requête traverse un reverse proxy |
X-HTTP-Method-Override | Indique la véritable méthode à considérer, utilisé lorsque le client ne supporte pas la vraie méthode (et utilise généralement le fallback "POST") |
Réponse :
Entête | Description |
Content-Disposition | Indique le nom de fichier à utiliser pour sauvegarder un téléchargement |
Content-Encoding | Indique l'algorithme de compression ou de chiffrement utilisé dans le contenu de la réponse |
Content-Length | Indique la longueur en octets du corps de la réponse |
Content-Language | Indique la langue utilisée dans le contenu de la réponse |
Content-Location | Indique l'URI d'origine de la ressource, utilisé lorsque l'URI de la ressource différe de la requête (exemple : alias) |
Content-Range | Indique la part de la représentation de la ressource figurant dans le contenu, utilisé dans une gestion de liste |
Location | Indique l'URI de la nouvelle ressource, utilisé lors d'une création (POST) pour fournir au client un moyen de récupérer la ressource fraichement créée |
HATEOAS
HATEOAS est un ensemble de principes qui peuvent s'appliquer aux styles d'architecture REST pour favoriser l'interopérabilité.
Ces principes ambitieux sont essentiellement fondés sur des capacités hypermédia du niveau 3 de maturité REST.
Une des idées fortes consiste à ajouter dans la réponse les éventuels liens associés à la ressource pour faciliter l'auto-découverte des services exposés. (plus d'infos)
option
Dans la plupart des cas, l'utilisation de HATEOAS est inapropriée et s'apparente à un luxe (non pragmatique).
GZip
devrait
Il est pertinent de supporter le format de compression gzip pour optimiser les performances.
La compression gzip permet de gagner de 60 à 80% de la bande passante.
Avantage supplémentaire : l'intérêt de "minifier" le JSON de réponse devient négligeable en terme de performance, il devient ainsi tout à fait acceptable de répondre avec un JSON "lisible" pour un humain (espaces, indentation, pretty print).
Pagination
option
Lorsqu'une réponse est constituée d'une longue liste ou d'un nombre d'éléments indénombrables il convient de mettre en place un système de pagination pour donner la possibilité au client de naviguer partout dans la liste et présenter à son utilisateur une interface pertinente.
Le principe de fonctionnement de la pagination repose sur la notion de curseur : on fait porter au client l'index courant (curseur) de la réponse obtenue qui représente un morceau dans une liste plus complète.
Pour implémenter la pagination, se réferer à la RFC 5988.
On fournit au client un entête (emplacement idéal pour les méta données), exemple :
Link: <https://api.niji.fr/contacts?page=5&size=10>; rel="next", <https://api.niji.fr/contacts?page=3&size=10>; rel="previous"
La ressource fournissant cet entête serait ici la page 4 de la liste des contacts.
Si l'on souhaite fournir en plus le nombre total de résultats, on utilise l'entête "X-Total-Count".
X-Total-Count: 150
Stateless
Le fait de maintenir une session dans une application web a d'importantes conséquences sur son comportement : fragilisation, surconsommation mémoire, nécessité de gérer un timeout, risque sécuritaire
doit
Une application propulsant une API se doit d'être stateless pour permettre la scalabilité du système.
Les APIs ne se conçoivent pas comme un site web, on ne préjuge pas du "parcours", on fournit des services : il n'y a aucune raison d'avoir besoin de mémoriser un état entre deux requêtes.
Si une telle situation se produit (cas exceptionnel, transaction longue, …), il convient de réfléchir à une solution alternative : on fait le plus souvent porter l'état par le client.
Sécurité
La sécurité dans un environnement APIsé est sujette aux mêmes règles que toute application web.
Cela recouvre le transport des données, l'authentification / les droits / l'accès aux ressources qui doit être contrôlé, ainsi que les bonnes pratiques de développement web (injections, sanitization, …).
La communauté OWASP constitue un bon référentiel pour recenser les principales failles applicatives et les bonnes pratiques de contre mesures associées.
SSL
doit
Toujours privilégier l'utilisation de SSL qui sécurise les échanges sur le plan de la confidentialité, et de l'authenticité du serveur fournissant l'API.
De plus, SSL est maintenant considéré par les moteurs de recherches référents comme un prérequis dans une stratégies SEO.
Authentification et accès
Le simple fait d'exposer une ressource suffit à considérer qu'elle sera consommée, même si à priori on ne connait pas sa localisation.
doit
Si la ressource est censée être en accès restreint, il est impératif de faire le nécessaire pour contrôler son accès, et renvoyer une erreur en cas de tentative d'accès irrégulière.
Les erreurs à renvoyer sont généralement de trois types :
- L'accès est interdit (code statut 401) : cas d'une tentative d'accès sans présentation de l'autorisation nécessaire
- L'accès est refusé (code statut 403) : cas d'une tentative d'accès avec potentiellement une autorisation valide, mais insuffisante (droits)
- L'accès est ignoré (code statut 404) : cas plus rare où l'on ne souhaite même pas informer le consommateur que la ressource existe (scope restreint, ressource d'administration)
Les mécanismes d'authentification courants sont : Basic Auth, OAuth2, X.509 ou encore HMAC, voire une simple clé en entête.
Le filtrage IP n'est pas un mécanisme d'authentification : on limite simplement l'exposition, en prenant de gros risques opérationnels (IP de proxy sortant partagée, plan d'IP sujet à évolution, voire non prédictible pour un cloud).
L'authentification par certificat est très efficace, sa mise en oeuvre est adaptée pour un serveur, mais s'avère lourde voire impossible pour le client; elle est par ailleurs potentiellement couteuse.
HMAC est une bonne alternative pour obtenir un niveau relativement équivalent au certificat, et présente l'intérêt d'être relativement light (donc adapté aussi à l'IoT).
OAuth2 est le standard le plus répandu dans le domaine du web, donc à privilégier pour une exposition massive et multi-canale (sauf pour l'IoT).
Le point clé : bien réfléchir aux contraintes induites sur le client qui va consommer l'API.
Cache
option
Si la ressource le permet (idempotence et durée de vie de la donnée maîtrisée), proposer un mécanisme de cache aux consommateurs visant à répondre rapidement sans avoir besoin de traverser toutes les couches de l'architecture logique pour obtenir les données (base, SI, …).
Les deux approches recensées pour implémenter une politique de cache sont : ETag et Last-Modified
Plus d'info
Articles pour s'acculturer davantage :