Niji RESTful API guidelines

Photo by Dan Clear on Unsplash

Niji RESTful API guidelines

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étiqueSignification

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 :

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éthodeActionSituationRemarques
GETlister, obtenirobtenir la représentation d'une ressourcele SI n'est pas modifié (idempotence)
POSTajouterajouter un élément dans une collection de ressourcesle SI est modifié et souvent un identifiant est généré pour la nouvelle ressource créée (et communiqué via l'entête Location)
PUTmodifier complètementremplacer une ressourcele 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
PATCHmodifier partiellementmettre à jour une ressource partiellementles données composantes de la ressource existante et non présentes dans le payload restent inchangées
DELETEsupprimer, purgersupprimer, détruire une ressource, ou vider une collection de ressourcesclassiquement la réponse ne contient pas de corps et le statut est 204
HEADvérifiervérifier l'obtention d'une ressource, obtenir des méta donnéesaucun 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
OPTIONSse renseignerobtenir des informations sur les modalités de la communicationpermet 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 :

RessourceNom
Une pizzapizza
Un clientcustomer
Le client 3customer?id=3
Le client 3customers/3
L'adresse du client 4customers/4/address
L'email 3 de l'utilisateur 2users/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 :

RessourceNom
S'enregistrersignup
Se connectersignin
Supprimer une page des favorispages/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 :

BesoinTraduction sémantiqueNommage résultant
Obtenir l'adresse de l'utilisateur 7Obtenir l'adresse de l'élément 7 de la collection des utilisateursGET /customers/7/address
Obtenir la facture de mon compte...GET /account/invoice
Créer un clientPoster un élément dans la collection des clientsPOST /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 :

BesoinRessource
Obtenir les pizzasGET /pizzas
Créer une pizzaPOST /pizzas
Obtenir la pizza carbonaraGET /pizzas/carbonara
Modifier la pizza carbonaraPUT /pizzas/carbonara
Supprimer la pizza carbonaraDELETE /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 à :

  1. Choisir le bon code statut adapté à la situation
  2. 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 :

  1. 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
  2. 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)
  3. l'entête de requête Accept : convention du format compliquée, pénible à traiter (application/vnd.masuperapi.v2+json)
  4. Aucun : toujours considérer la dernière version, avec le risque d'instabilité du contrat
  5. 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ètreEffetExemple
selectpermet de ne sélectionner qu'une portion du JSON en appliquant un filtre sur les attributs?select=age:35,pays:france
unselectle contraire de select, filtre le JSON en ne retenant que les items qui ne matchent pas aux critères?unselect=nom:dupont
sorttrier la collection d'objet sur le critère d'un attribut (préfixe "-" pour un ordre inversé)?sort=-departement
pickne conserver que les attributs spécifiés?pick=nom,prenom,age
omitsupprimer 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êteDescription
AcceptType de média accepté par le client pour la réponse (exemple : application/json)
Accept-LanguageLangue acceptée par le client pour la réponse (exemple : da)
AuthorizationFournit une autorisation (Basic Auth, token OAuth, HMAC, ...) éventuellement requise par l'API
Content-LengthIndique la longueur en octets du corps de la requête
Content-TypeIndique le type de média du corps de la requête
X-Powered-ByIndique 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-CacheIndique si la réponse est issue d'un cache (HIT) ou non (MISS), à utiliser avec une politique de cache
X-Forwarded-ForFournit 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-OverrideIndique 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êteDescription
Content-DispositionIndique le nom de fichier à utiliser pour sauvegarder un téléchargement
Content-EncodingIndique l'algorithme de compression ou de chiffrement utilisé dans le contenu de la réponse
Content-LengthIndique la longueur en octets du corps de la réponse
Content-LanguageIndique la langue utilisée dans le contenu de la réponse
Content-LocationIndique l'URI d'origine de la ressource, utilisé lorsque l'URI de la ressource différe de la requête (exemple : alias)
Content-RangeIndique la part de la représentation de la ressource figurant dans le contenu, utilisé dans une gestion de liste
LocationIndique 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 :

  1. L'accès est interdit (code statut 401) : cas d'une tentative d'accès sans présentation de l'autorisation nécessaire
  2. L'accès est refusé (code statut 403) : cas d'une tentative d'accès avec potentiellement une autorisation valide, mais insuffisante (droits)
  3. 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 :