Jerome Boileux
Yousign Engineering & Product
6 min readNov 7, 2023

--

Retour sur l’une des décisions parmi les plus structurantes que nous avons prises en tant qu’équipe de développeurs.

Le contexte

Dans notre application, la communication entre le serveur et le client se fait en utilisant une API HTTP JSON privée.

Lorsque nous avons commencé à développer notre application, nous générions un fichier openAPI à partir des annotations dans les fichiers PHP. Ce fichier était exposé et nous servait de documentation pour notre API. La partie cliente était alors du simple Javascript, et la question des modèles de données ne se posait pas de ce côté-là.

Ensuite, la guilde front a pris la décision, plus que judicieuse, d’utiliser TypeScript, un langage à typage statique que l’on ne présente plus. Très rapidement après avoir entamé notre migration, le besoin de typer les données consommées depuis l’API s’est présenté. Redéfinir “manuellement” ces modèles en TypeScript dans la partie front de la code base n’était pas une option, car nous avions besoin d’un couplage fort avec l’API pour garantir la validité de cette donnée.

> 👉 Nous devions donc générer automatiquement les types à partir de la spécification openAPI.

Grâce à la communauté openAPI & TypeScript, nous avons rapidement trouvé les outils / packages qui nous permettent cette génération. Notre choix s’est porté sur `openapi-generator-cli` qui permet de générer des clients axios et les modèles séparément.

Lors des premières utilisations du code généré, nous avons rapidement réalisé que la spécification ne correspondait pas toujours à l’implémentation.

Cela s’explique par le fait que la spécification est générée depuis des annotations et non pas depuis le code PHP.

Voici quelques exemples de problématiques que nous avons rencontré à cause de cette désynchronisation :

  • La plupart des ressources qui composent les réponses ont des propriétés optionnelles spécifiées, mais elles sont obligatoires dans leur implémentation.
  • Une propriété est utilisée, mais n’existe pas dans la spécification openAPI.
  • Certains payloads ne sont pas spécifiés du tout, alors qu’ils existent dans l’API.
  • Certains types sont simplement erronés dans la spécification par rapport à l’implémentation (`array` au lieu d’`object`).

Les options

Option #1 : Réparer les annotations dans le code PHP

Réparer les annotations existantes et continuer dans cette voie pourrait fonctionner.

Avantages :

  • L’équipe n’a qu’à examiner les annotations et les corriger.
  • Nous restons dans la zone de confort en conservant les processus actuels, rien n’est nouveau.

Inconvénients :

  • Si l’annotation a été désynchronisée de l’implémentation réelle, elle pourrait être désynchronisée à nouveau. Tout simplement parce que c’est possible.
  • Les annotations peuvent apparaître accessoires pour le développeur
  • La spécification de l’openAPI est étroitement liée au langage PHP et aux annotations.
  • Il n’y a pas de place pour la collaboration autour de la spécification de l’API ; la spécification doit être écrite par un développeur back-end lors de l’écriture de l’implémentation et la responsabilité de l’API se retrouve donc uniquement coté backend.

Option #2 : Extraire la spécification openAPI dans son propre répertoire pour en faire la source de vérité

L’autre solution consisterait à changer de paradigme en écrivant d’abord la spécification openAPI, dans son propre projet, totalement découplée de toute mise en œuvre.

Avantages :

  • Comme l’openAPI serait écrite en premier, et avant tout développement, cela lui donnerait plus de pouvoir et en ferait la source de vérité *conceptuelle*. L’implémentation devrait suivre la spécification, et non l’inverse comme c’est souvent le cas (l’annotation suit l’implémentation).
  • Permet la collaboration, n’importe quel développeur front-end serait en mesure de rédiger la spécification.
  • Totalement découplée de toute mise en œuvre dans un langage spécifique.
  • Plus de place pour de nouveaux processus, comme la spécification openAPI serait le premier code à écrire avant toute fonctionnalité avec des mises à jour de l’API, et pourrait être révisée en binôme sans bruit.
  • Plus de liberté lors de l’écriture de la spécification de l’API — il n’y a pas de DSL intermédiaire (annotations) qui pourrait nous limiter.

Inconvénients :

  • Selon l’outil PHP utilisé, la spécification pourrait techniquement être désynchronisée de la mise en œuvre.
  • Nouveaux processus et nouveaux outils à apprendre.
  • Selon de la manière dont nous écrivons l’API (interface graphique ou code), certains d’entre nous pourraient avoir à apprendre de nouveaux DSL (Domain-specific language).
  • Comme la spécification est autonome, la colocation entre l’implémentation (PHP) et la spécification (annotation) disparaîtrait.

La décision

Il y a de multiples raisons de penser que l’option #2 nous donnera la spécification d’API la plus propre et la plus précise, et c’est donc celle que nous préférons.

En rédigeant la spécification avant tout développement d’API, et en l’isolant des autres domaines dans la codebase, nous pensons que les gens s’en occuperont mieux — aidés par un processus formel.

Cette décision a fait l’objet d’un ADR (Architecture Decision Record), car elle impacte plusieurs sujets centraux chez nous :

  • notre processus de développement
  • la conception de l’API
  • la collaboration entre les développeurs
  • la capacité des squads à délivrer de la valeur au cours de leurs itérations

Aujourd’hui, lorsqu’une squad démarre le développement d’une fonctionnalité, elle commence par rassembler un quorum d’acteurs concernés autour d’une merge request qui définie les changements apportés à l’OAS (OpenAPI Specification).

Une fois celle-ci mergée, les développeurs backend disposent du contrat et peuvent effectuer les assertions nécessaires dans leurs tests pour garantir le respect de la spécification, tandis que les développeurs frontend disposent d’un client TypeScript et des modèles associés que seront consommés par le code applicatif.

🗺️ Nous avons alors besoin d’un plan pragmatique et détaillé pour atteindre notre objectif.

L’implémentation

Écrire le squelette

La première étape consistera à rédiger un squelette d’openAPI avec quelques routes. Une nouvelle fonctionnalité, par exemple, fera office de candidat idéal.

Détails de mise en œuvre :

  • L’API sera écrite en OAS 3.0.0
  • La spécification sera écrite en YAML (directement dans un éditeur de texte ou par exemple avec Stoplight Studio.
  • La spécification sera située dans le dossier /oas à la racine de l’application

Ajouter une librairie avec les modèles TS générés

Le fait d’avoir une spécification correcte et précise nous permet de générer des modèles pour le frontend.

Ceci sera fait en utilisant OpenAPI Generator (avec le client typescript-axios)

Détails de l’implémentation :

  • Créer une nouvelle librairie dans notre monorepo “@yousign/api”
  • La source de cette librairie contiendra tous les types générés par openapi-generator basé sur notre OAS.
  • Cette librairie sera construite comme une dépendance de l’application principale et exportera les modèles d’API de l’OAS ainsi que les clients axios.

Assurer la qualité de la spécification en utilisant notre CI

Afin d’avoir une spécification correcte, nous devons linter le fichier `app.yaml` dans notre CI.

Nous utiliserons Spectral comme CLI.

Un nouveau job est ajouté dans les pipelines de CI “test/oas” pour contrôler la qualité :

  1. Linter le fichier OAS avec la spécification définie dans le dossier.
  2. Générer des types et faire des diff avec ceux déjà présents dans le package front.

Valider la compatibilité front avec l’API

En générant des types TS avant de construire l’application, nous assurerons la compatibilité.

Utilisation de l’orchestration de tâches “turborepo” pour générer des types et vérifier le code.

La suite

L’OAS est devenu une brique centrale dans le fonctionnement des équipes et toutes les routes s’y trouvent désormais. Souvent, c'est la première merge request qui est produite conjointement entre backend et frontend lors du démarrage d’une nouvelle fonctionnalité. Cela constitue la spécification pour le backend qui va tester son développement depuis l’OAS au moment d’écrire les nouvelles routes. Coté frontend, cela permet de commencer à implémenter les fonctionnalités plus tôt en utilisant les clients auto-générés.

Le socle est solide et pourrait nous permettre de construire d’autres initiatives prochainement. Par exemple, il pourrait s’avérer pertinent de générer automatiquement nos handlers MSW et nos mocks à partir de l’OAS pour nos tests d’intégrations… à suivre.

--

--