Un simple middleware avec Deno Runtime et la librairie Oak

Introduction

Le succès de Node.js n’est pas à mettre en doute. Le fameux runtime basé sur le moteur V8 de Chrome est aujourd’hui très fréquemmen tutilisé côté back-end.

Son architecture non bloquante permettant le traitement de nombreuses requêtes, ses performances élevées, l’utilisation du langage Javascript déjà fortement plébiscité côté front-end, mais aussi son appui sur l’écosystème npm, et enfin sa relative(!) simplicité - la première chose que l’on apprend lorsque l’on se penche sur Node.js n’est-elle pas qu’il permet de mettre en place un serveur http en moins de dix lignes ? - ont certainement été des facteurs essentiels à son adoption par une large communauté de développeurs.

Cependant, des critiques ont pu se faire entendre : sécurité, gestion des erreurs, typage faible, etc...

On lit ainsi de temps en temps que Node.js est mort ! Loin de nous toute sensibilité à ces annonces sensationnalistes, mais la curiosité ne doit pas nous faire défaut, et c’est justement à l’un des potentiels substituts à Node.js que nous nous sommes intéressés : le déjà célèbre Deno, dont la courbe de popularité a connu une croissance significative depuis son introduction en 2018, même s’il est important de noter qu'il est encore relativement nouveau et n'a pas atteint le même niveau de popularité que son grand frère Node.js.

Deno vs.Node.js

Créé par Ryan Dahl, le père de Node.js, Deno est un environnement d'exécution JavaScript et de runtime pour l'exécution de programmes écrits en TypeScript et en JavaScript. Il reste proche de Node.js mais se distingue de ce dernier par plusieurs caractéristiques clés. Voici les points importants à connaître sur le runtime Deno :

 

  • Sécurité : Deno met l'accent sur la sécurité en offrant un modèle de sécurité par défaut plus strict que Node.js. Par exemple, Deno ne permet pas l'accès au système de fichiers ou au réseau par défaut. Les autorisations d'accès doivent être accordées explicitement lors de l'exécution du programme
  • TypeScript intégré : Deno     est construit avec une prise en charge native du langage TypeScript. Cela signifie que vous pouvez écrire du code en TypeScript et l'exécuter     directement dans Deno, sans avoir besoin de transpiler en JavaScript. Cependant, Deno prend également en charge l'exécution de programmes     JavaScript standard.
  • Modules ES : Deno prend en charge les modules ECMAScript (ES) nativement, ce qui signifie que vous pouvez importer des modules directement à partir d'URLs. Deno utilise également le système de fichiers local pour stocker les modules téléchargés, ce qui facilite la gestion des dépendances.
  • Gestion des dépendances : Deno a une approche différente de la gestion des dépendances par rapport à Node.js. Au lieu d'utiliser npm et le fichier package.json, Deno utilise des URLs pour spécifier les dépendances directement dans le code. Cela permet d'éviter certains problèmes liés à la version des dépendances. Plus     de dossier node_modules ici.
  • Outils intégrés : Deno inclut plusieurs outils utiles intégrés, notamment un debugger, un formateur de code et un linter. Ces outils facilitent le développement et     l'entretien des programmes.
  • Support de l'API Web : Deno offre une prise en charge native de l'API Web, ce qui permet d'écrire des serveurs HTTP et des applications web directement dans Deno, sans avoir besoin de frameworks externes.

Notre POC

Pour nous lancer dans une prise en main de Deno, nous nous sommes donnés pour objectif de réaliser un petit middleware permettant d’insérer des chats dans une base de données MySQL (en attendant les requêtes MiaouwSQL qui seront sans doute mieux adaptées ! ).

Pour notre exercice, nous avons, outre Deno et une base de données, utilisé les librairies Oak et mysql, faisant toutes deux parties des Third Party Modules de Deno.

 

Oak est une librairie permettant de rapidement mettre en place un middleware et de gérer des routes, dans le même esprit qu’Express pour Node.js, par exemple. Oak fournit un ensemble d'outils pour la création de serveurs HTTP et la gestion des requêtes et des réponses.

 

La librairie mysql permet quant à elle tout simplement de se connecter et d’interagir avec une base MySQL, en utilisant la syntaxe classique de requêtage.

 

Principales étapes

Passée une simple installation de l’environnement Deno et l’habituel test deversion :

deno --version , nous sommes prêts à écrire nos premières lignes de code.

Le fichier index ne dépaysera pas les utilisateursde Node.js:

import { Application } from "https://deno.land/x/oak/mod.ts";
import { APP_HOST, APP_PORT } from "./config.ts";
import router from "./routes.ts";
import _404 from "./controllers/404.ts";
import errorHandler from "./controllers/errorHandler.ts";

const app = new Application();

app.use(errorHandler);
app.use(router.routes());
app.use(router.allowedMethods());
app.use(_404);

console.log(`😺 Server started`);
console.log(`🚀 Listening on ${APP_HOST}:${APP_PORT}`)

await app.listen(`${APP_HOST}:${APP_PORT}`);

La méthode use de l’objet Application (librairie Oak) permet d’ajouter facilement les différentes composantes de notre middleware.

L’importation de la librairie Oak donne un bon exemple de la gestion des dépendances :

import { Application } from "https://deno.land/x/oak/mod.ts";

On voit qu’on fait un lien direct vers la source web. La syntaxe utilisée ci-dessus nous permet d’ailleurs d’accéder systématiquement à la dernière version de la librairie. Si l’on souhaite figer notre import sur une version spécifique, nous pouvons écrire :

 import { Application } from "https://deno.land/x/oak@v12.5.0/mod.ts";

Par ailleurs Oak nous donne accès un objet Router , qui nous permet de très simplement définir nos routes :

import { Router } from "https://deno.land/x/oak@v12.5.0/mod.ts";
import addCat from "./controllers/addCat.ts";
import getAllCats from "./controllers/getAllCats.ts";
import getCat from "./controllers/getCat.ts";
import updateCat from "./controllers/updateCat.ts";
import deleteCat from "./controllers/deleteCat.ts";

const router = new Router();

router
    .get("/", (ctx) => { ctx.response.body = "Hello Cats!" })
    .post("/cats", addCat)
    .get("/cats", getAllCats)
    .get("/cats/:id", getCat)
    .put("/cats/:id", updateCat)
    .delete("/cats/:id", deleteCat);

export default router;

Notre client MySQL est extrêmement simple :

import { Client } from "https://deno.land/x/mysql@v2.11.0/mod.ts";

export const mysqlClient = await new Client().connect({
  hostname: '127.0.0.1',
  port: 8889,
  username: 'root',
  password: 'root',
  db: 'deno_cat_db',
});

Ceci est un chat  :

class Cat {
  constructor(
    name: string, 
    details: string, 
    age: number, 
    picture: string | undefined,
    id?: number | undefined, 
    ) {
    this.id = id;
    this.name = name;
    this.details = details;
    this.age = age;
    this.picture = picture;
  }

  id?: number | undefined;
  name: string;
  age: number;
  details: string;
  picture: string | undefined;
}

export { Cat }

Et voici comment on attrape un chat  :

import { Status } from 'https://deno.land/std@0.188.0/http/http_status.ts';
import { getCat } from '../services/catService.ts';
import { Response } from "https://deno.land/x/oak/mod.ts";

export default async ({ params, response }: { params: { id: string }; response: Response }) => {
    const catId = params.id;

    if (!catId) {
        response.status = 404;
        response.body = { msg: 'Invalid cat ID' };
        return;
    }

    const foundCat = await getCat(catId);

    if (!foundCat) {
        response.status = 404;
        response.body = { msg: `Cat not found` };
        return;
    }

    response.status = Status.OK;
    response.type = "json";
    response.body = { msg: `Found cat with id ${catId}`, foundCat };
}

Oak nous donne accès à un objet Response, qui nouspermet de construire facilement notre réponse.

Côté repository, cela donne :

import { ExecuteResult } from "https://deno.land/x/mysql@v2.11.0/mod.ts";
import { mysqlClient } from "../db/mysql_client.ts";
import { Cat } from "../models/cat.model.ts";

  async getCat(id: string): Promise <Cat> {
    const cat: ExecuteResult = await mysqlClient.execute(
      'select * from cats where ?? = ?',
      [
        'id', id,
      ]
    );
    return cat.rows?.at(0);
  }

Comme on le voit, la librairie mysql permet sans complication de faire exécuter une requête par notre client exposé plus haut.

Enfin, last but not least, comment ça se lance ?

Une simple commande :

deno run --allow-all --watch index.ts

Il est important de préciser que, suivant les accès demandés par notre application (par exemple l’accès à Internet), une simple commande deno run index.ts ne fonctionnerait pas. Pourquoi ?

Revoici la question relative à la sécurité, évoquée en début d’article : Deno nécessite qu’on lui accorde diverses autorisations suivant nos besoins. Un accès au réseau Internet nécessite par exemple d’indiquer le paramètre --allow-net dans notre commande.

Ici, pour des raisons de simplicité, nous avons opté pour une liberté totale avec un paramètre --allow-all, nous accordant par là toutes les permissions possible.

 

Enfin, très pratique, le paramètre --watch redémarre automatiquement le serveur à chaque modification du code !

Conclusion 

Vincent : la principale difficulté que j’ai rencontrée est l’adaptation au langage Typescript, que je ne pratique pas quotidiennement. La logique de Deno est proche de celle de Node.js, et l’on avance en terrain connu pour peu que l’on ait quelques rudiments de ce dernier.

Luc: Deno apporte des améliorations par rapport à Node.js qui peuvent être très utiles sur des projets importants, mais aussi augmenter le temps de mise en place sur des petits projets.

 

Les points  :

  • simplicité de mise en place,
  • encouragement au typage fort avec Typescript,
  • sécurité renforcée,
  • meilleure gestion des dépendances,
  • erreurs plus compréhensibles.

 

Les points  :

  • la migration d’un projet Node vers Deno n’est pas automatique et demande un travail d’adaptation,
  • Deno n’est pas 100% compatible avec les packages Node.

Luc ALLARDIN - Développeur Mobile

Vincent LE BRAS - Développeur Full Stack