Comment créer une bonne API Web - Partie 1

Posté le 11. October 2020 dans Programmation

Bonjour,

Je souhaite vous parler de l'écriture d'API. Je vais découper cet article en 3 parties:

Je me limiterai au WEB et aux normes REST et GraphQL même s'il y a d'autres normes/frameworks pour écrire des API.

Commençons donc par le début ! Qu'est-ce qu'une API ? API signifie Application Programming Interface. C'est une interface de programmation prête à être consommée par un client.

Par exemple quand on développe une librairie (en C/C++, voir un module NodeJS, ...), on définit une liste de méthodes que l'on rend publique et qui sont utilisées pour appeler cette librairie. Ces méthodes sont alors utilisées par des clients. L'API c'est ce contrat entre la librairie et le client.

Dans le cadre d'un site internet, l'API est le contrat entre un site internet et le client qui l'appelle. Comme tout contrat il faut que celui-ci soit clairement défini si on veut que ça se passe bien entre les clients et les services.

Il est important pour une bonne API public:

  • d'être stable dans le temps, ceci afin qu'un client qui utilise une API puisse continuer à l'utiliser sans devoir tout réécrire entre chaque version,
  • d'être fiable, cela va sans dire mais une API qui retourne des erreurs une fois sur deux ou qui ne fait pas ce qu'on lui demande va vite être abandonnée,
  • d'être simple d'utilisation et bien documentée, afin que le développeur passe le moins de temps possible à apprendre à l'utiliser,
  • d'avoir une bonne gestion des erreurs,
  • d'être performante.

Dans le monde du web, il existe plusieurs protocole/norme afin de faciliter l'écriture de ce contrat d'interface, on peut citer par exemple:

  • SOAP (Simple Object Access Protocol)
  • REST (Representational State Transfer)
  • XML-RPC (XML Remote Procedure Call)
  • GraphQL (créé par Facebook)
  • Falcor (qui est un protocole basé sur une implémentation de référence ; créé par Netflix)

Il existe plein de manières d'écrire des API. On peut aussi en écrire à base de socket réseau. Dans cet article je ne vais pas détailler toutes les méthodes possibles mais je vais présenter uniquement une selection d'API Web (Rest et GraphQL).

Pourquoi écrire une API Web

Pour commencer si votre application web est une SPAspa, il sera nécessaire d'avoir un contrat d'interface entre le client (SPA) et le serveur (sauf s'il n'y a pas de serveur...). Ce contrat est généralement implicite.

Même pour un site web statique, ou pour un site dont les pages HTML sont générées coté serveur, nous pouvons considérer que l'API (l'interface de programation exposée) de l'application sont ses pages HTML. En effet, il existe des web scrapper qui lisent le contenu HTML des pages afin d'en lire le contenu (voir même qui en expose une API plus compréhensible).

Du coup, la question la plus importante est "Faut-il que mon interface de programmation soit publique ou privée ? Faut-il qu'elle soit simple d'utilisation".

Pour un site, dont les pages sont générées côté serveur et dont le rendu est en HTML, l'API n'est ni simple d'utilisation, ni documentable. Pour une SPA (Single Page Application), l'écriture d'une API utilisable par un programme est la norme et elle sera alors consumée par Angular, Vue, React, jQuery ....

Souhaite-t-on alors que notre application soit utilisable publiquement ?

Souhaite-t-on que d'autres applications développées par des tiers puisse utiliser notre API, librement ? gratuitement ?

Si oui, dois-je mettre en place une politique sur le nombre de requêtes maximales par utilisateur (OUI) ? Et de combien ? Vais-je pouvoir supporter la charge ?

Prenez en considération dans votre décision que de tout facon, les gens feront ce qu'ils veulent. Après tout, si un humain peut visualiser la page, un programme le peu également (WebScrapper).

communique avec le serveur via ses API. C'est en quelque sorte une application lourde mais écrite en JavaScript. Au démarrage du site l'interface (le client) est chargée en mémoire (par morceau si on prend le lazy loading) et l'interface est ensuite générée à la volée. Les pages HTML sont donc générées côté client et non coté serveur.

Qu'est qu'une bonne documentation

Si on veux que l'API soit utilisée, il faut qu'elle soit bien documentée et surtout à jour. Sans cela personne ne voudra l'utiliser.

Pour faciliter l'écriture de la documentation certains outils permettent de générer celle-ci à partir du code (des types de données, du nom des méthodes, d'annotation ...), ce qui permet de maintenir plus facilement sa documentation avec l'évolution du code. Par contre, l'utilisation de générateur ne dédouanne pas de l'écriture de la partie qui ne peux pas être documentée (comme les descriptions, les exemples, ...).

Swagger WoodstockBackup

La documentation doit contenir :

  • des exemples d'appels
  • des descriptions détaillées de ce que fait chaque point d'entrée,
  • quels type de données sont présentes en entrée et comment les utiliser,
  • mais aussi le type des données que l'on a en sortie,
  • les erreurs que peut retourner le endpoint et dans quelles circonstances,
  • comment seront traitées les données en entrée (batch, immédiat).

Au début de la documentation, ne pas oublier de documenter:

  • le fonctionnement de l'authentification
  • les règles d'utilisation

    • ce qu'on est autorisé à faire
    • ce qu'on n'est pas autorisé à faire
    • le nombre d'appel par seconde/minutes/heures/...et quels sont les headers HTTP permettant de récuperer le résultat (par exemple dans l'API Github, on peut retrouver les headers: X-RateLimit-Limit, X-RateLimit-Remaining)
    • le fonctionnement de la pagination dans l'API

Il peut être efficace d'avoir une page au démarrage de l'API (Quick Start) donnant un première exemple complet d'un premier appel à l'API.

Dans le développement de l'API, l'écriture de la documentation est aussi important que l'écriture du code.

Gestion des erreurs

Pour la gestion des erreurs, l'API doit retourner le maximum d'informations pour que le développeur puisse comprendre l'erreur et effectuer une correction mais également suffisament d'informations pour que le développeur puisse les utiliser dans son programme pour retourner les problèmes fonctionnels à l'utilisateur final.

Par exemple dans une API Rest, il est important que les différents cas d'erreur soit explicités:

  • 400 - BadRequest: The request is malformed.
  • 404 - NotFound: The resource backup can't be found
  • 401 - Unauthorized: The user is not authentified.
  • 403 - Forbidden: The user is not authorized to access to the resource backup.

Ceci afin d'aider les développeurs à traiter tout les cas d'erreurs et d'afficher un message cohérent à l'utilisateur.

Lors d'une erreur, le retour de la requête doit toujours être le même. Voici un exemple de requête:

GET http://192.168.101.205:3000/api/hosts/unknownhost

{
  "statusCode": 404,
  "message": "Can't find configuration for the host with name unknownhost",
  "error": "Not Found",
  "errorCode": "HOST_NOT_FOUND",
  "params": {
      "host": "unknownhost"
  }
}

Dans l'exemple ci-dessus, errorCode peut être utilisé pour indiquer à l'utilisateur, un champ obligatoire, (avec dans params le nom du champ) ou une règle de gestion mal utilisée. Ce code erreur pourra alors être remontée à l'utilisateur final directement pour rendre l'interface plus réactive.

Cohérence

Pour faire une bonne documentation il faut également de bonnes bases. Pour cela il faut que l'API soit cohérente dans son fonctionnement. La cohérence est importante tant au niveau des paramètres que dans le contenu de la requête ou de la réponse.

Par exemple, sur une API REST, imaginons que pour gérer la pagination un endpoint demande les paramètres skip et limit:

GET /hosts/pc-ulrich/backups?skip=5&limit=10

{
    "skip": 5,
    "limit": 10,
    "size": 100,
    "result": [
        {
            "id": 1
        },
        {
            "id": 2
        }
    ]
}

N'utilisez pas pour un autre endpoint de votre application des paramètres différents. De même, n'utilisez pas dans le résultat de la requête des noms différents de ceux utilisés dans les paramètres.

Enfin structurez le contenu de la réponse toujours de la même manière, pour que vos utilisateurs puissent développer des méthodes génériques lors de l'utilisation de votre API (Par exemple une méthode de pagination générique).

GET /hosts?start=5&size=10

{
    "debut": 5,
    "taille": 10,
    "fin": 100,
    "liste": [
        {
            "id": 1
        },
        {
            "id": 2
        }
    ]
}

Cela aurait comme conséquence de perdre les utilisateurs (développeurs) qui utiliseront votre API.

Les structures, les champs utilisés sur les différents endpoint de votre API doivent toujours suivre la même logique et par exemple:

  • nommer toujours les champs ayant la même fonction de la même manière: toujours nommer la date de création createdAt,
  • toujours retourner la resource demandé directement ou inversement toujours encapsuler la resource demandée dans un sous-objet

    {
      "result": [
        {
          "id": 1
        },
        {
          "id": 2
        }
      ]
    }

    vs

    [
      {
        "id": 1
      },
      {
        "id": 2
      }
    ]
  • toujours gérer la pagination de la même manière dans la réponse (par des entête http, ou directement dans le body, mais surtout avec les mêmes noms de champs).
  • toujours gérer les erreurs de la même manière.

Gérer le versionning de l'API

Si vous souhaitez changer le fonctionnement de l'API, surtout ne cassez pas la cohérence de l'API, ni la version utilisée par tous. Pour cela vous avez plusieurs choix, et ils sont complémentaires:

  • Vous pouvez créer une nouvelle version de l'API.

    En créant une nouvelle version de l'API (passage de /api/v1/... à /api/v2/...), vous vous assurez que l'ancienne version fonctionne toujours et que les clients pourront migrer doucement de l'ancienne version vers la nouvelle.

    Si vous souhaitez casser la cohérence de l'API (par exemple remonter les infos skip et limit du body dans des headers X-Pagination-Skip et X-Pagination-Limit), vous pouvez le faire une à une sur chaque API.

    Pensez à un plan de décommissionnement pour ne pas maintenir 50 versions d'API différentes. Prévenez vos utilisateurs que les anciennes versions vont être décommissionées avec une date raisonable pour qu'ils puissent modifier leurs applications.

  • Lors de la création d'une nouvelle API, il peut être intéressant de déprécier les attributs de l'ancienne API, cela vous permet de prévenir l'utilisateur qu'il utilise un champ déprécié et qu'il sera décommisionné.

    L'ajout d'attribut ne pose généralement pas de problème, mais cela peut vous permettre de supprimer des attributs après avoir veillé à prévenir vos utilisateurs de leur obsolescence future sans forcément créer une nouvelle API.

    Par contre cela nécessite de faire des modifications peu structurantes.

    Pour des modifications plus structurantes vous devrez alors créer des nouveaux endpoint au coeur de l'API au risque de perdre l'utilisateur.

  • Monitorer votre API et son utilisation (analytics).

    Cela vous permettra de savoir si vos API sont utilisées, leurs taux d'utilisation.

    Etudier le taux d'utilisation vous permet ainsi de savoir le risque à décommissionner une API, mais aussi peut vous motiver à améliorer les API les plus utilisées.

    Avec certains framework (GraphQL) il est même possible de monitorer l'utilisation des attributs de votre API. Cela vous permet de plus facilement décommissionner les attributs de votre API au fur et à mesure de leur dépréciation.

Pour le client

Une bonne API doit être écrite pour le client qui va l'utiliser.

Il est important de ne pas créer son API en se basant sur son modèle interne mais en créant son API sur son usage.

Un appel d'API peut nécessiter plusieurs appels internes, et inversement plusieurs API peuvent faire appel aux mêmes données internes. C'est au serveur derrière l'API ensuite de gérer un système de cache (avec invalidation de celui-ci) pour limiter les appels à son back, ou sa base de données trop régulièrement.

De la même manière certains champs internes peuvent nécessiter d'être transformés pour être intégrés ou pour être exposés.

Certaines données internes n'ont parfois même pas besoin d'être exposées.

Du coup il faut se poser les bonnes questions lors de la création de son API :

  • Qui sont les clients qui vont appeler mon API ?
  • Quelles cinématiques utilisateurs se cachent derrière ses clients ?
  • Lors de chaque appel de quelles données auront besoin les clients ?

    • Dois-je regrouper certains champs dans une même resource pour limiter le nombre d'appels ?
    • Dois-je déplacer certaines données dans des sous-resources pour limiter la quantité de données lors d'un appel ?
    • Dois-je ajouter de la pagination, de la projection, de la recherche pour limiter la quantité de données qui ressortira de mon API ?

Il faut prendre en compte qu'en fonction de l'appelant le résultat peut être différent. Une application mobile ne présentera pas forcément les données de la même manière qu'un site WEB. Est-ce que l'API doit savoir répondre aux deux ? (Certains framework savent mieux répondre à ce genre de questions que d'autres).

Conclusion

Si vous avez aimé cet article, je vous invite à lire la suite qui paraitera bientôt.


  1. une SPA (Single Page Application) est une application dont l'interface Web est entièrement écrite en javascript et qui