REST API avec Node/Express + OpenAPI

Pas de révolution ici, c’est quelque chose somme toute d’assez classique, un back-end NodeJs en Express et une doc Swagger basé sur un OpenAPI. Le truc étant de faire le routage et la doc en un seul point, améliorant la maintenabilité et automatisant le routage.

Pour ce faire il suffit de démarrer un nouveau projet et d’installer quelques librairies de base :

npm init
npm i express --save
npm i swagger-parser --save
npm i cors --save
npm i swagger-routes-express --save
  • Le framework express comme base,
  • La lib swagger-parser pour lire et interpréter le fichier openapi.yaml (définition de l’API),
  • La lib cors pour définir l’autorisation,
  • La lib swagger-routes-express pour créer un connecteur reliant les contrôleurs au routeur basé sur la définition de l’API,
  • La lib swagger-ui-express pour mettre à disposition un swagger de la définition de l’API, sur une route /api-docs.

Maintenant qu’on a la base il nous faut le squelette d’application, du coup au lieu de taper tout le code ici je t’invite cher lecteur à te rendre sur ce GitHub, et nous allons détailler les parties intéressantes.

OpenAPI

D’abord, qu’est-ce que l’on veut accomplir ? Dans cet exemple on va simplement faire un service REST pour obtenir une liste d’items, un GET. Nous allons donc décrire un fichier openapi.yaml décrivant cela.

openapi: 3.0.0
info:
  description: service backend
  version: 1.0.0
  title: my-api
paths:
  /items:
    get:
      summary: Get all items
      description: Get all items
      operationId: getItems
      responses:
        "200":
          description: success
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/item'
servers:
  - url: /api/v1
components:
  schemas:
    item:
      type: object
      properties:
        id:
          type: number
        name:
          type: string
        date:
          type: string

En gros : on définit une route /items qui renverra un tableau d’item et la structure item. Jusque là c’est du formalisme standard que vous retrouverez un peu partout.

Le serveur

Ensuite il faut construire notre serveur (je n’utiliserai pas le générateur), pour cela nous aurons principalement 2 fichiers de base et un fichier contrôleur. Le schéma étant assez simple, on reçoit une requête, le routeur fait un match et associe la méthode du contrôleur.

On commence par écrire un fichier makeApp.js (dans /src), qui correspond à la définition de notre serveur, notre application. Dedans on y trouve beaucoup de choses, on y reviendra plus en détail après, mais en gros on décrit ce que nous avons dit plus haut, à savoir : définir un serveur express auquel on va connecter le routeur, lui-même basé sur le fichier openapi.yaml décrit plus haut. Le connecteur du routeur se lie aux contrôleurs, nous y reviendrons.

const express = require('express')
const SwaggerParser = require('swagger-parser')
const swaggerRoutes = require('swagger-routes-express')
const ctrls = require('./controllers')
const cors = require("cors")

const makeApp = async () => {
  const parser = new SwaggerParser()
  const apiDescription = await parser.validate('./api/openapi.yaml')
  const connect = swaggerRoutes.connector(ctrls, apiDescription)
  const app = express()

...

  // Connect the routes
  connect(app)

  // Add any error handlers last

  return app
}
module.exports = makeApp

Ensuite on a le fichier point de départ : index.js, que l’on placera dans un répertoire /src, du coup n’oubliez pas de modifier package.json avec la propriété main :

"main": "./src/index",

Ainsi que la commande start, celle ci se lancerait avec un npm run start mais vous pourriez avoir une surprise, via un Git Bash sur Windows, d’avoir une erreur sur le ./

  "scripts": {
    "start": "./src/index.js",

Dans ce fichier index.js on aura un appel au précédent fichier de config.

const makeApp = require('./makeApp')
const port = 3000;

makeApp()
  .then(app => app.listen(port))
  .then(() => {
    console.log(`App running on port ${port}...`)
  })
  .catch(err => {
    console.error('caught error', err)
  })

Le contrôleur

Nous avons vu que le serveur se lie aux contrôleurs, il est maintenant temps de nous y intéresser. Nous allons créer un répertoire controllers dans /src, et un fichier item.js permettant de regrouper par domaine les actions concernant les items. À côté on créer un fichier index.js permettant de lister le contenu du répertoire au niveau de l’appelant, une façon de faire également utilisée en typescript/Angular.

Le fichier item.js

exports.getItems = async (req, res, next) => {
  res.json([]);
};

Le fichier index.js

const { getItems } = require('./item')

module.exports = {
  getItems
}

On peut voir la fonction getItems dont le nom match l’attribut operationId du fichier openapi.yaml. Évidemment c’est voulu 🙂 Et c’est important de garder ça à l’œil quand vous préparez votre YAML. Actuellement la méthode renvoie un tableau vide.

Compléter le serveur

Avant de vouloir tester il nous manque un morceau, en fait plusieurs petits détails. Pour faire des appels à notre API, depuis notre poste, on aura un soucis de CORS, mais aussi, potentiellement de cache (en-tête etag) et d’url encoding. À cela on veut préciser que l’on traitera du JSON dans les échanges.

  // Options
  app.set("etag", false); //turn off
  app.use(
    express.urlencoded({
      extended: true,
    })
  );

  app.use(express.json());

  app.use(cors({
    origin: '*'
  }))

Tester

Pour tester on peut lancer le serveur avec : node src/index.js

On peut ouvrir un Bash et lancer un curl de test, tel que :

$ curl -s localhost:3000/api/v1/items
[]

Du coup bonne nouvelle on a quelque chose qui fonctionne et répond correctement, à savoir une réponse d’un tableau vide ([]). Pour le fun on peut structurer une donnée et la renvoyer, en respectant le modèle proposé dans le YAML.

exports.getItems = async (req, res, next) => {
  res.json([
    {
      id: 1,
      name: "Item 1",
      date: "2022-07-13"
    },
    {
      id: 2,
      name: "Item 2",
      date: "2022-07-01"
    }
  ]);
};
$ curl -s localhost:3000/api/v1/items
[{"id":1,"name":"Item 1","date":"2022-07-13"},{"id":2,"name":"Item 2","date":"2022-07-01"}]

Ajouter la documentation Swagger

D’abord il va nous falloir une lib en plus :

npm i swagger-ui-express --save

Ensuite on va ajouter les lignes suivantes dans notre makeApp.js, l’une dans les déclarations, l’autre après les options afin de déclarer une route de documentation et lancer Swagger.

const express = require('express')
const swaggerUi = require("swagger-ui-express");

...

  // This is the endpoint that will display the swagger docs
  app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(apiDescription));

Couper/relancer le serveur et rendez-vous sur l’url localhost:3000/api-docs.

Illustration du résultat du Swagger

Et ensuite ?

À partir de là libre à vous de créer des services, connecter une base de données, gérer du fichier, etc.

Liste des ressources utilisées

L’enfer est pavé de bonnes intentions

Notre premier projet étant fait, et le second ayant des besoins commun, j’ai voulu créer ma première librairie et ainsi la partager entre mes projets comme le fait NPM si je publiais. Évidemment je suis sur un git privé, pas sur Github, ni sur NPM, du coup pour le déploiement etc. ben, c’est toute une histoire…

Donc on part d’un code fonctionnel inclus dans un folder de mon app, on crée un nouveau projet de type librairie et on colle ça dedans, on nettoie le premier projet et on fait le lien entre les libs, on compile l’un et l’autre, l’impatience est à son comble ! Et ça ne va pas…

Donc on part d’une lib de référence (Material Design) et on regarde comment ils font, on s’en inspire, on prend les conventions et on observe les liens, on compile etc. Le stress est palpable ! Et ça ne va pas…

Là, le doute s’installe, malgré les lectures officielles, des articles, des mails, un stackoverflow, un appel à l’aide général et de la patience… rien n’y fit. Et la raison reste un mystère.

Que ça soit l’inclusion d’une lib en local, ou via son git perso, que ça soit un module qui ne va pas alors que l’autre oui, la folie s’installe… et surtout le rejet de ce méchant projet, ce qui le met en péril de progression.

Après avoir accepté l’échec de cette noble tentative et un gros FUCK à Angular et leur système « y a qu’les hypsters qui croivent » (comprendre : une élite peu nombreuse et souvent prétentieuse). Ô rage et désespoir… Enfin donc une idée surgit de la bonne vieille école des barbus, à l’ancienne mais restons propre.

Donc on part d’un code fonctionnel inclus dans un folder de mon app et on crée un répertoire vide en dehors et on tape le code dedans sans lui faire de mal, on ajoute un index.ts et on check que tous les liens de notre point d’entrée sont bons et accessibles.

On adapte notre projet avec le tsconfig.json et le package.json. On ajoute un script de post installation et on peut résumer ainsi : je git clone mon projet ou un nouveau et je fais le npm install, il rapatrie les node_modules nécessaires, ça on connait, il lance tout seul comme un grand le script de postinstall et va cloner notre « lib maison » sous forme de répertoire à l’intérieur de notre projet (ajouté au .gitignore), ensuite grâce aux modif du tsconfig.json, il suffit de faire une référence unique vers cette lib « comme » si c’était une lib NPM.

On respecte donc l’énoncée :

  • Chaque projet doit avoir son repo git
  • La « librairie » est facilement maintenable
  • Les projets utilisent la « librairie » facilement
  • L’ensemble ne se gêne pas

Dans le détail, dans package.json :

"postinstall": "git clone ssh://user@server.net/blabla.git src/mon-folder" 

Ajouter votre répertoire au .gitignore de votre projet :

#mon folder
/src/mon-folder

Vient ensuite votre tsconfig.json :

"baseUrl": ".",
...
"paths": {
  "@ma-lib/*": ["src/mon-folder/*"]
}

Enfin, l’usage dans votre app.module.ts :

import { MonModule } from '@ma-lib/index'; 

Un mois… ça aura pris un mois, mais on peut enfin continuer…

Et de un, maintenant à nous deux !

Ça y est, Schema Definer a été fini le week-end passé ! Le temps de redescendre du dernier rush, de valider les points et tâches dans le Redmine (système de tickets). On respire et on prend du recul.

Nous avons désormais le moyen de paramétrer notre schéma, sa définition. Nous allons pouvoir l’utiliser et en faire Folkopedia, ce qui reste le but de tout ceci. Cependant, ce serait vendre une voiture sans avoir assembler les pièces, cela reste décousu, non-défini.

Quand on fabrique un site web manipulant des données, nous utilisons des outils pour regarder à quoi ressemble les données stockées et de les manipuler. Il nous permet de savoir à quel résultat on s’attend à la sortie, et si ce n’est pas le cas d’être aidé à comprendre.

Dans notre cas spécifique ce même outil ne peut nous aider qu’à voir notre schéma et, de manière éparse, nos entités et attributs. Si l’on doit travailler avec il nous faut un outil équivalent capable d’utiliser notre schéma. Ainsi, par équivalence, nous aurons un outils d’inspection et de manipulation des entités créées.

J’ai nommé ce projet Entity Manager car le but à ce niveau est bien de travailler au niveau des entités et de leur attributs. Par extension cela concerne également leurs relations et la visibilité des contenus, ce qui couvre la totalité du schéma dans son état présent.

Voici donc un projet qui se glisse entre deux, mais qui ne ralentira que peu Folkopedia, car ce qui sera développé dans Entity Manager, telle qu’une partie de Schema Definer, pourront être mis en commun et ne pas devoir être réécrit. Cela peut paraître évident, mais pas pour tout le monde, ni rendu simple par les technologies utilisées et dans notre contexte privé.

Je vais donc techniquement partir sur une librairie Angular partagée entre nos projets qui composent Folkopedia. Pour les connaisseurs, il ne s’agit pas ici d’un projet monolithique-monorepository mais bien d’app séparée par projet ayant une lib privée en commun. La documentation étant quasi inexistante, comme par hasard, nous revoilà en quête d’impossible et de challenges d’évidences.

Links to DRY véier

Deux blocs ont été fait entièrement, les deux plus simple pour commencer et concevoir les éléments nécessaires.

Tout d’abord Visibility Level, niveau de visibilité de l’entité associée, qui a permis de mettre en place le CRUD (Create, Read, Update, Delete) requis. Ainsi sur les 5 endpoints prévu, 4 sont utiles actuellement et ont été réalisé : lister l’ensemble, ajouter, éditer et supprimer.

Pour la suppression, comme vous le savez ou l’espérez, cliquer sur le bouton ne doit pas faire l’action sans une confirmation. Généralement on vous affiche une boîte de dialogue pour vous demander de confirmer, d’une manière ou d’une autre cela vous fait regarder ailleurs ou vous masque ce vous faites.

Material Angular n’a pas de mécanique de popover tel que Bootstrap a, il n’a qu’un tooltip discret. Du coup j’en ai conçus un.

Confirmation

Alors évidemment il est personnalisable : nombre de boutons, valeur, couleur, tooltip et texte (label). Il na pas encore la fonctionnalité de position modifiable (haut, gauche, droite), il va juste dessous celui qui en a besoin, ici un bouton de suppression à confirmer.

Il verrouille le bouton cliqué, affiche sa boite élégante et attend votre clique, à côté il ferme la boute et relâche le bouton verrouillé et sur un bouton renvoie la valeur. Simple, efficace et réutilisable ! Car oui j’en ai fait un module et une directive, tout en un tant qu’on y est et qu’en plus on l’a jamais fait 🙂 mouahahaha la blague…

Vous trouverez pas mal de tuto mais aucun avec les 2 ensembles, et évidemment, aucun complet… Pensez à regarder après ‘entryComponents‘ dans votre fichier app.module.ts à l’occasion quand vous ferez ça ;).


Ceci dit, j’ai également fait le bloc Link Types, les types de lien ! 🙂 Il a une petite particularité, une variable en plus, du coup ça permet d’y aller mollo en difficulté.

Par contre les 2 suivants seront d’un coup plus complexe : défis accepté !


Il y a aussi quelques changements côté back : des tests en plus suite à l’oubli du check d’unicité durant les updates, et évidement le code de test ajouté. On a +100 tests avec ~200 comparaisons ^^ et une couverture inchangée actuellement, toujours 95% !

Autre modification back : un complément pour la liste des Link Types qui ne répondait pas parfaitement à la description de l’API. Détails, mais c’est mieux avec.

Sur le Front rouge

Après plus de douze heures à creuser ma tranchée et à établir mon avant-poste, un bivouaque, un feu et ma radio, voici enfin venu le temps de faire le compte-rendu de ce week-end.

Nous y sommes, sur le Front du combat que l’on mène depuis bientôt presque un mois, au devant du combat, dans le feux de l’Action, loin du doux cocon de l’API. Enfin, pour ceux qui ont suivi, c’était loin d’être un simple doux cocon… ah ah !

Comme prévu, à l’image de schema.org, voici l’allure de notre Schema Definer fraîchement créé. Simple et concis, de quoi parcourir les définitions et, à terme, de les éditer.

Pour faire un point technique, et justifier les 12h dites en début d’article, il a fallu commencer par mettre mon poste à jour et comme toujours non sans mal avec @angular/cli; Node ça été tout seul, et une fois le CLI (Command Line Interface) installé à la bonne version c’est Angular qui a suivi. Nous voilà donc avec un environnement propre Angular 8 et Material Design pour le fun et l’inconnue.

Pour ceux que ça intéresse, faites gaffe avec Material Design, cela ne remplace pas Bootstrap (grille, typo, style etc), mais ne concerne que les composants (bouton, tableau, formulaire, …). En bref, faites attention.

S’en est suivis une mise en place du projet et de l’authentification avec service et gardien (guard). Je vous passe les emmerdes et vous redirige vers quelques articles qui m’ont bien aidé/inspiré.

La liste n’est pas exhaustive. C’est surtout le premier qui m’a bien aidé à comprendre la mise en place de la mécanique du gardien en Angular, ainsi que les interceptor et quelques opérateurs rxjs.

Vu la progression du week-end, voici un extrait représentatif du travail accompli depuis le début du mois (le détail serait beaucoup trop long), et qui, ce week-end, a fait un beau bon en avant.

Maintenant que la machine est en place, le reste devrait suivre sans nouvelle mauvaise surprise. Enfin, j’espère…

PatAPI et patapon

J’ai tout donné aujourd’hui et l’API du Schema Definer est finie ! enfin pour la version prévue sur le plan, on est d’accord. Du coup le titre…

J’ai ajouté le Code Coverage (couverture du code par les tests, ça montre ce qu’on a « testé » ou non) et on a +91% !

Les lignes rouges c’est où il n’y a rien, donc le pourcentage n’est pas exact, mais c’est pas grave, on est bon et ça fait plaisir, il n’a pas été aisé d’en arriver là (voir les 3 dernière lignes, c’est là que ça se passe en fait).

90 tests, sans compter les 10 de la base de données, comptant 187 comparaisons (assertions). Il y a de la redondance et des comparaisons de trop, c’est certains, mais quand on sait que rien que pour tester les Attributs et ses valeurs listées j’ai ~500 lignes de code de test…

J’ai aussi passé pas mal de temps sur la définition de l’API avec son fichier RAML dont je parlais dans un article précédent. Du coup je pense que la documentation est à jour et valable. Pour ceux que ça intéresse, ça prend pas mal temps, on négligera mais n’y penser même pas, ça vaut le coup. Pensez également à votre PHPDoc.

Le temps d’écrire cet article, de faire une pause et chipoter, on est passé à 96 tests et 193 comparaisons avec un résultat de ~94% de couverture ! Des commentaires en plus, du refactoring pour réaménager des méthodes qui pouvaient être mieux écrites, cibler les interdépendances des tests (@depends), etc.

Ça y est, notre API est prête, l’instant de vérité approche. Nous allons pouvoir commencer le « front » (site visible/pour les humains autorisés) et mettre à l’épreuve les appels, dans le sens « est-ce qu’on a pensé à tout ? », « est-ce suffisant ? » et/ou « est-ce utile/nécessaire ? ».

Et ainsi tu typeras

Une semaine plus tard voici un état stable comprenant l’authentification ET les types d’entité !

C’est avec un soulagement plus qu’une fierté que je vous annonce que les types d’entité, côté API du Schema Definer, sont réalisés ! Ye_ah (mou).

On a donc le set qui sera le même que pour les autres fonctionnalités prévues, soit : obtenir la liste de tous, un seul avec détails, suppression, édition et création.

On a un joli total de tests avec 29 tests et 63 validations. Épuisant mais enrichissant, tant pour la qualité du projet que pour les connaissances approfondies du système que cela donne.

En avant pour les 4 blocs restant, toujours côté API. Viendra ensuite le front-end, la partie visible sous forme de site.

Oh API BDay

Après un premier résultat et son échec, nous revoilà dans la course ! Au menu nous avons une authentification, un contrôle middleware de token (~session), un logout, et un logout de toutes les connexions. Le tout testé et non sans mal !

On respire, on se détend et on regarde le point suivant, toujours dans un contexte Schema Definer : la création de type d’entité. C’est parti !

Oh API day

Petit point sur l’avancement des travaux concernant le Schema Definer. Non je n’ai pas abandonné et oui ça progresse ! Non mais, je vous vois là dans le fond.

Actuellement je n’ai bossé que sur l’API et ses tests, et point encore sur le front Angular. Plus précisément sur la partie Auth et Logout, ce qui implique le middleware (~ détection de votre connexion courante), le tout en stateless (~ pas de session).

Enfin, quand je dis « que », ce n’est pas rien, c’est important et cela m’a pris pas mal de temps. D’une part, pour « bien » faire, et d’une autre car la doc de Lumen est une catastrophe au final, ainsi que sa communauté, quand on sort des rails… ontologie vous vous rappelez ?

Ce n’est surement pas encore parfait mais ça fonctionne suivant les règles établies par le cahier des charges. De plus c’est testé, avec PHPUnit, et ça non plus ça n’a pas été aisé, notamment sur les appels JSON pour tester de bout en bout, il a fallu pas mal creuser entre les docs.

Du coup petit topo, on a déjà 3 classes de tests, représentant un total de 24 tests pour 75 contrôles réparti en 2 « suites de tests ». Ça prends forme et ça m’a bien aidé, même « quand ça va pas », vos erreurs sont attrapées par le systèmes et ne vous aide pas toujours, un peu comme Eloquent et sa foutue classe Connection pour ceux qui connaissent…

On va donc pouvoir attaquer les différents services à fournir et travailler sur le système de droits. C’est parti !


Edit

Après avoir fièrement écris cet article, j’ai voulu continuer comme dit, et d’un pas décidé je regarde ma spec de l’API pour attaquer dans l’ordre. Et ô stupeur, concernant le sujet Auth, j’ai oublié une idée, mais non sans conséquences : la déconnexion de toutes les connexions de l’utilisateur.

Pour vous ce n’est peut-être rien, mais aujourd’hui vous vous connecté potentiellement au même site via votre GSM, votre PC ou votre tablette en même temps et ceci ça se gère !

Actuellement la connexion était unique, si vous vous connectez sur votre GSM, par sécurité on déconnecte la « sessions » de votre PC, etc. Sauf que non, vous voulez peut-être démultiplier les onglets et moyen de connexions pour une raison X ou Y et c’est à nous de vous le permettre, en le gérant.

Du coup, notre connexion unique, elle doit devenir multiple, et comment on fait en ontologie/dans notre schéma ? Et ben on doit le modifier et donc réécrire le code, pas toute la logique mais une bonne partie, et donc les tests spécifique à la mécanique.

C’était fini ? Et ben plus maintenant, on recommence…

Pour les curieux, la connexion doit devenir une Entité, car elle comporte plus d’un attribut (token, ip, …), il faut donc défaire ce qui concerne le token etc par rapport à l’entité de l’utilisateur, créer une nouvelle entité, lier les attributs, créer le lien vers l’user et refaire les requêtes. C’est là le côté moins performant qui s’affiche, mais c’est là aussi que l’on voit la flexibilité de l’usage sans changer la base de données elle-même.

Documentation d’API avec RAML

Toujours dans l’optique de documenter le plus proprement et intelligemment possible le projet Folkopedia, j’ai voulu m’occuper de la définition de l’API concernant Schema Definer.

Tout d’abord j’ai pensé un Google Sheet/Doc, mais, bien que fonctionnel, ce n’est pas le plus sexy ni adapté en terme de documentation.

Il existe des solutions comme ApiDoc, Swager, etc. et RAML sur lequel je me suis arrêté. L’idée n’est pas d’avoir un générateur d’API mais un moyen de documenter. Donc un fichier structurant me permettant d’avoir une sortie sous forme de documentation, ici un HTML dynamique.

Comme si c’était notre credo, la documentation stricte du site de base raml.org n’est pas tout à fait bonne, peut-être une confusion entre la v0.8 et la 1.0. Du coup, belotte et rebelotte, vous aurez à croiser quelques liens.

https://github.com/raml-org/raml-examples/blob/master/defining-examples/organisation-api.raml

Dans mon cas je suis sous Visual Studio Code pour éditer mon fichier raml. Il y a l’extension RAML 3.0.1 de blzjns nécessitant raml2html à installer en console (besoin de node) via la commande :

npm i -g raml2html

Pour ma part le preview interne ne donne pas un résultat idéal, je vous conseil donc de compiler vous même votre HTML avec la commande :

raml2html api.raml > api.html

Du coup j’en profite pour vous montrer un exemple fonctionnel et pas fini.

#%RAML 1.0
title: Folkopedia API
baseUri: http://api.folkopedia.com/{version}
mediaType: application/json

types:
  Auth:
    properties:
      login: string
      code: string
  Error:
    properties: 
      code: string
      message: string
  User:
    properties:
      firstname: string[]
      lastname: string[]
      nickname: string[]

/auth:
  post:
    description: Authenticate a user
    body:
      application/json:
        type: Auth
        example: { login: "contact@folkopedia.com", code: "myPassw0rd" }
    responses: 
      201:
        body:
          application/json:
            type: object
            properties:
              token: string
              identity: User
              acl: string[]
            example: |
              {
                token: "auth_token",
                identity: {
                  firstname: ["Jehan"],
                  lastname: ["Bihin"],
                  nickname: ["Killan", "Pôlebreak"]
                },
                acl: ["acl1", "acl2", "acl3"]
              }

Ce qui une fois compilé nous donnera ce type de rendu plutôt sympa.

Oui c’est en UTF-8 mais ça n’a pas bien géré le ô :s

Tout doucement une solution complète se met en place et ce n’est pas faute de l’avoir simplifié à la base. Tant mieux ça fait de l’expérience ! Et de belles prises de têtes en tête à tête ^^. Y a du boulot…