Comment documenter son API ReST au mieux ?

Temps de lecture estimé : 6 minutes

De nombreuses questions traversent la conception d’une API : quel protocole ? avec quelle sécurité ? quelles sont les actions possibles, avec quelles informations et sur quelles ressources ? Chaque réponse doit être intégrée au code et dûment documentée pour aider le consommateur à savoir comment interagir avec l’applicatif. Ça ne paraît rien comme ça, mais cette documentation est un enjeu central : si l’on dit qu’une API est aussi bonne que sa documentation, c’est qu’elle est la condition du succès de l’API elle-même. Or, la documentation a toujours été le parent pauvre de l’informatique. En format texte, elle est éminemment imparfaite : comme les techniciens trouvent ça ennuyeux et superflu, elle a un vrai risque d’être décorrélée de l’API elle-même… réduisant son intérêt à zéro.

La spécification OpenAPI part du constat que les questions structurantes sont toujours les mêmes et propose une norme de réponses à ces questions afin de maximiser la correspondance code <> documentation à tout instant. Pour découvrir ce que cette spécification API a à nous offrir, nous allons concevoir une application de gestion de livres et mettre en place ici trois éléments socles d’une API : la requête, la réponse et la sécurité. Notre application manipule du json et permet :

  • pour un lecteur anonyme : rechercher des livres selon un titre, un auteur ou sa classification de Dewey et consulter un livre spécifique par son ISBN ;
  • pour un libraire connecté : ajouter un livre par son ISBN.

Le descripteur OpenAPI openapi.yaml compte quelques champs obligatoires. Commençons par les décrire :

openapi: 3.1.0 # Décrit la version de la spécification suivie par le document, pour la validation
info:
  title: |
    Cette api est une application simple qui expose les routes d'une gestion de livres. Elle sert à la fois aux lecteurs et aux libraires.    
  version: 1.15.24 # Version de l'application (format libre)

Pour la suite, on va séparer par cas d’utilisation : d’abord le lecteur, ensuite le libraire.

Le lecteur

Allons-y pas à pas. Le cas le plus simple est l’atteinte d’un livre unique par son ISBN et il n’existe que deux réponses possibles : soit on a trouvé le livre et on le retourne avec le code HTTP 200, soit le livre n’existe pas et l’API retourne le code HTTP 404. Sur OpenAPI, toute déclaration de route doit être imbriquée dans la clé paths. Dans notre cas, notre critère de recherche ISBN est directement embarqué dans la route ; le tout s’écrit donc ainsi :

paths:
  /livre/{isbn}:

Cette route de recherche s’appuie sur un paramètre qui n’est pas explicité. Nous devons donc spécifier le verbe ReST associé et donner des détails sur la teneur du paramètre via la clé parameters :

paths:
  /livres/{isbn}:
    get:
      parameters:
        - name: ibsn
          in: path
          required: true # Puisque le critère est dans le chemin, il est forcément obligatoire
          schema:
            type: string
            pattern: '^\d{13}$'

NB: L’ISBN n’étant pas un format commun comme date ou byte, on se rabat sur pattern pour apporter une première vérification. La validation de l’ISBN est déléguée à l’application.

La requête étant terminée, passons maintenant aux réponses possibles. Ces réponses ont pour clés les codes HTTP retournés, imbriqués sous la clé responses. Seul le champ description est obligatoire, mais c’est le champ content qui est le plus important, car c’est lui qui va porter l’information principale : « que retourne-t-on au consommateur ? ».

Le « 404 » n’est pas très important, attardons-nous donc sur le cas nominal. Dans notre contexte, un livre c’est plusieurs choses :

  • un ISBN,
  • un titre,
  • des auteurs,
  • une classe Dewey

L’objet schema est un peu le couteau-suisse d’OpenAPI. Nous l’avons utilisé plus haut et nous allons également l’exploiter pour représenter formellement le livre :

paths:
  /livres/{isbn}:
    get:
      # [...]
      responses:
        "404":
          description: "Désolé mario, mais le livre est dans un autre château !"
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  isbn:
                    type: string
                  dewey:
                    type: string
                  titre:
                    type: string
                  auteurs:
                    type: array
                    items:
                      type: object
                      properties:
                        nom:
                          type: string
                        prenom:
                          type: string

Pour la recherche de livre, le fonctionnement n’est pas bien différent. Nous avons des parameters, mais qui sont cette fois en query string et la réponse est sous la forme d’un tableau :

paths:
  /livres:
    get:
      parameters:
        - name: titreLike
          in: query
          schema:
            type: string
        - name: auteurNomLike
          in: query
          schema:
            type: string
        - name: dewey
          in: query
          schema: # Permet de rechercher sur plusieurs classes, 'dewey[]=dewey1&dewey[]=dewey2
            type: array
            style: form
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  # description d'un livre

Nous cherchons ici des livres correspondants aux classes Dewey passées en paramètres et nous permettons des approximations sur le titre et le nom d’auteur. À nouveau, l’implémentation de ces recherches reste à la main de l’application en dessous.

Le libraire

Le libraire quant à lui peut ajouter un livre à la collection en publiant un contenu grâce à la clé requestBody, suite à quoi il reçoit deux retours possibles : HTTP 400 si la requête est mal formée, HTTP 201 si tout s’est bien passé.

paths:
  /livres:
    post:
      requestBody:
        content:
          application/json:
            schema:
              # description d'un livre
      responses:
        "400":
          description: "Le champ xxx est incorrect"
        "201":
          description: OK

Mais au-delà de la description simple du point d’entrée, il y a une chose qui distingue le libraire du lecteur : ses opérations sont authentifiées. La sécurité est une partie importante de la conception de toute API et la nôtre ne fait pas exception. Dans son objectif d’universalité, OpenAPI permet de préciser le fonctionnement de l’authentification / autorisation, cela par la spécification de formats standards : clé d’API, JWT, mTLS, Oauth ou OpenID. Ici, nous allons en combiner deux à titre d’exemple.

La sécurité s’écrit en deux temps. Dans le premier temps, il est nécessaire de modéliser les formats offerts par l’API au moyen de la clé components.securitySchemes, dans laquelle on précise nos schémas autorisés :

components:
  securitySchemes:
    bearerJwt: # Forcément en header
      type: http
      scheme: Bearer
      bearerFormat: JWT
    key:
      type: apiKey
      in: header # Les valeurs possibles sont header, query, cookie
      name: Api-Key

Et une fois cette définition faite, on peut ajouter ces formats à notre route sécurisée :

paths:
  /livres:
    post:
      security:
        bearerJwt: []
        key: []
      # [...]

Cette route impose ainsi une authentification pour passer, là où les autres sont librement accessibles.

Un descripteur, mais pas seulement

La création d’une API simple nous a fourni un support pour découvrir comment écrire une spécification OpenAPI. Mais si le fichier openapi.yaml ne s’arrête qu’à ça, la « spécification » n’est rien de plus qu’une autre doc, subissant de fait exactement le même problème que dénoncé en intro. La force d’OpenAPI est son formalisme : si on l’appelle spécification, c’est parce qu’elle explicite un ensemble d’exigences à satisfaire et que ces exigences sont développées par un organisme collaboratif, l’OpenAPI Initiative. De ce fait, en plus de formaliser les attendus (ex: qu’est-ce qu’est un livre ?), cette norme permet entre autres à d’autres outils d’implémenter ces descriptions pour automatiser les tests et validations de cette API, offrir une documentation ergonomique et actionnable directement héritée du code, voire surveiller les ruptures de compatibilité. Les bénéfices sont nombreux.

Mais alors, qu’est-ce-qui permet à OpenAPI de maximiser la correspondance code <> documentation évoqué plus haut ? C’est ce que nous verrons dans un prochain article.

Pour visualiser une documentation ergonomique et actionnable de notre API d’exemple, vous pouvez retrouver le code pour essayer par vous-même sur le dépôt d’exemple.


Source :


    Ce billet vous a plu ? Partagez-le sur les réseaux…


    … Ou inscrivez-vous à la newsletter pour ne manquer aucun article (Si vous ne voyez pas le formulaire, désactivez temporairement uBlock).

    Voir aussi