REST API avec Node/TS/Express + OpenAPI

Évolution de la solution précédente reprise à zéro. Donc un serveur Node avec Typescript ce coup-ci, dont les routes sont pilotées par l’openAPI.

On commence avec le tuto bien foutu « How to set up TypeScript with Node.js and Express » de LogRocket. Sur lequel on va reprendre la solution précédente morceau par morceau, et c’est là que ça devient intéressant.

En résumé, nous aurons 2 @types à ajouter, ainsi que concurrently pour essayer, lors des installs pour que ça fonctionne et adapter les 2 codes principaux.

npm i -D concurrently
npm i @types/cors
npm i @types/swagger-ui-express

Il faudra également faire attention à votre git bash, dans le fichier .bash_profile ou rc, activer le support des couleurs pour concurrently.

export FORCE_COLOR="1"

Le makeApp s’adapte sous forme d’une classe TS

import SwaggerParser from "@apidevtools/swagger-parser";
import SwaggerRoutes from "swagger-routes-express"
import express from "express";
import cors from "cors"
import swaggerUi from "swagger-ui-express"

export class MakeApp {
    static async make() {
        const parser = new SwaggerParser()
        const apiDescription = await parser.validate('./api/openapi.yaml')
        const connect = SwaggerRoutes.connector({
            getCore: async (req, res, next) => {
                console.log("getCore")
                res.json([])
            }
        }, apiDescription, {
            onCreateRoute: (method, descriptor) => {
                const [path, ...handlers] = descriptor
                console.log('created route', method, path, handlers)
            }
        })
        const app = express()

...

        return app
    }
}

Et l’index de bootstrap légèrement adapté

import { MakeApp } from "./makeApp";
import dotenv from "dotenv"

dotenv.config()

const port = process.env.PORT || 3000

MakeApp.make()
    .then(app => app.listen(port))
    .then(() => {
        console.log(`[server]: Server is running at http://localhost:${port}`)
    })
    .catch(err => {
        console.error('Caught error', err)
    })

Pour les détails je vous invite à consulter le GitHub du projet ;). L’API a été simplifiée un peu et l’ajout de nodemon et concurrently.

Pas de contrôleur séparé dans ce test, flemme, tout simplement, le test est fonctionnel et on verra si je le pousse plus loin en terme de comparatif.

Être piloté par openAPI ou par le code et générer son swagger ?

C’est la question, vous trouverez la seconde option partout mais j’aimais l’idée que l’on puisse architecturer de manière agnostique son API et l’implémenter ensuite en collant les méthodes aux routes. Il est vrai que pour les mises à jour fréquentes, générer la doc sur base du code est plus simple, mais l’erreur est plus vite faite aussi me semble-t-il.

« Bricoler ainsi » ou framework prêt à l’emploi ?

Là encore, bonne question, ici j’ai un truc qui fonctionne, dans le bon esprit Node mais qui reste un assemblage dépendant de paquets divers et qui ne seront plus mis à jour (vérifié pour certains). De par leur nature et pour quoi ils ont été conçus, et pour ce que l’on souhaite faire avec dans le futur. Du coup j’ai regarder les frameworks TS tel que Nest ou Foal. Reste à déterminer avec 1 test de chaque lequel correspondrait au projet cible :).

Nest basé sur l’idée Angular est très populaire donc je trouverais facilement de la doc, quant à Foal c’est le petit nouveau full TS prometteur avec pas mal de built-in, mais à comparer avec le même type de test que cet article, en y ajoutant l’auth et la sécu pour les besoins clefs de base. Une connexion db et tester leur ORM pourrait également être un bon point et faire un article ensuite :).

Liste des ressources utilisées

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