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 :
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.
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 commedate
oubyte
, on se rabat surpattern
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 :
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 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.
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).