Développement Web

Programmation serveur avec Express

Romuald THION

Semestre pair 2022 UNC

Le framework Express

Express

Références

Express a des successeurs plus modernes comme https://www.fastify.io/ ou https://koajs.com/ mais reste une référence avec une très bonne documentation.

Exemple : une application Express de base

const express = require("express");
const app = express();
const port = 5000;

app.get("/", (req, res) => {
  res.send("Hello World!");
});

app.listen(port, () => {
  console.log(`Listening at http://localhost:${port}`);
});

En Express, comme dans la majorité des frameworks web, on va définir les étapes du traitement des requêtes et de production de réponses par des callbacks passés en paramètres.

MVC - Model View Controler

Le patron de conception (design pattern) Modèle-Vue-Contrôleur (MVC) est un modèle d’architecture logicielle :

  • une solution générale permettant de résoudre un problème classique
  • simplifie la maintenance et l’évolution du logiciel

Dans le domaine du génie logiciel, le livre fondateurs des design patterns est le Gang Of Four. Le MVC est un pattern, qui en compose en fait plusieurs.

Principe général du MVC : séparer

  • l’accès aux données : le modèle,
    • typiquement, via un drivers de BD ou un ORM
  • l’interface : la vue,
    • typiquement, via un moteur de template
  • traitements : le contrôleur
    • la logique de contrôle, en Express : les middlewares et routeurs

Mise en œuvre un peu particulière dans le cas des pages web : c’est le navigateur qui est responsable du rendu, de l’affichage

Le MVC dans Express

MDN

Les middlewares

Permettent d’enchainer les traitements des requêtes HTTP via des calllbacks de la forme function(request, response, next).

Source Writing middlewares

Un middleware peut :

  • exécuter du code arbitraire synchrone ou asynchrone
    • e.g. const promise = readFile(fileName);
  • modifier les objets requêtes et réponses HTTP
    • e.g., response.setHeader("X-Api-Version", "2.0.1");
    • e.g., response.locals.user = request.user
  • terminer le cycle requête-réponse
    • e.g., response.json({ code: "OK" }
    • e.g., response.status(403).end()

Un middleware peut passer la main au prochain middleware, trois façons :

  • next() : le prochain dans la liste courante de middlewares
  • next("route") : on sort de la liste courante et on continue
  • next(error) : le prochain middleware d’erreur
    • un middleware d’erreur prend quatre paramètres
      • function(error, request, response, next)

En Express, tout est middleware, voir using middlewares

Types de middleware

  • Application-level middleware
    • prend généralement tous les verbes HTTP, toutes les routes
    • utilisé avec app.use()
  • Router-level middleware
    • défini tun ensemble de routes et de verbes associés
    • on le branche sur un prefixe de route, comme /api ou /users
    • ce que l’on fait le plus souvent
  • Error-handling middleware
    • fonction function (err, req, res, next)
    • défini généralement en fin de la chaine de traitements

Besoin d’une fonctionnalité ?

Chercher un (bon) middleware qui l’implémente !

Un exemple de middleware

Middleware function requestTime.

Ajoute un champs à l’objet Response et passe au middleware suivant via next(). Si next() est oublié : pas de réponse pour le client !

function requestTime(request, response, next) {
  response.locals.requestTime = Date.now();
  next();
}

app.use(requestTime);

Un middleware async

Attend et passe au middleware suivant via next().

import setTimeout as wait from "timers/promises";

app.use(async function waiter(request, response, next) {
  await wait(1000);
  next();
});

Middleware configurable

Pour avoir un middleware configurable, il faut créer une Higher-Order Function (HOF) type décorateur

const setHeaders = function (headers) {
  return function (request, response, next) {
    response.set(headers);
    next();
  };
};

app.use(setHeaders({ "Warning": "this is sloppy", "X-API-KEY": "deadbeef00" }));

La chaîne d’évaluation

En enregistrant un middleware, on ajoute une étape à une liste de traitements. Pour chaque étape il y a :

  • une route sur laquelle elle s’applique, définie par une RegExp
  • un ou plusieurs handlers, qui peuvent être eux-mêmes des middlewares
    • un middleware définit à son tour une liste de traitements
      • c’est donc une liste de listes (de listes, de listes etc.)
      • l’ordre compte
import indexRouter from "./routes/index";
import usersRouter from "./routes/users";
import errorsRouter from "./routes/errors";

app.use("/", indexRouter);
app.use("/users", usersRouter);
app.use("/errors", errorsRouter);

Une requête traverse les étapes à partir du début et dans l’ordre :

  • next(), next(error) et next("route") permettent de passer aux étapes suivantes
  • resp.send() permet d’envoyer la réponse attention à la suite
    • essayer d’émettre une deuxième fois une réponse provoque une exception !
      • [ERR_HTTP_HEADERS_SENT]:Cannot set headers after they are sent to the client`
    • même si resp.send() est async, souvent on fait return resp.send() pour être sûr de casser le flot de traitement

Exemple de stack pour une application de démonstration (la liste peut avoir une vingtaine d’éléments et certains, à leur tours des dizaines de handlers) :

[
  ...
  Layer {
    handle: [Function: bound dispatch],
    ...
    route: Route { path: '/', stack: [Array], methods: [Object] }
  },
  Layer {
    handle: [Function: bound dispatch],
    ...
    route: Route { path: '/:code', stack: [Array], methods: [Object] }
  },
  Layer {
    handle: [Function (anonymous)],
    ...
    route: undefined
  }
]

Cas d’erreur dans la chaine d’évaluation

Selon le cas :

  • synchrone : si vous levez une exception, Express la récupère
    • e.g., throw new Error("KO");
  • asynchrone : il faut passer l’erreur à next(erreur)
    • sinon l’erreur ne sera pas attrapée et c’est le crash avec unhandledRejection
    • e.g. Promise.reject(new Error("async throw")).catch(next);

Voir https://expressjs.com/en/guide/error-handling.html

Les vues

On va déléguer la production de HTML à un moteur de templates, qui sont des équivalents à Jinja mais dans l’écosystème JavaScript :

Express permet de choisir et configurer le moteur

Exemple EJS

Intégration des principaux moteurs template à Express

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Document</title>
  </head>
  <body>
    <h1><%= title %></h1>
    <ul>
      <% for(const user of users){ %>
      <li><%= user %></li>
      <% }; %>
    </ul>
  </body>
</html>
const app = express();
app.set("view engine", "ejs");

app.get("/", async (request, response, next) => {
  const users = await database.getUsers();
  response.render("home", { title: "Welcome Home!", users });
});

Le modèle

Accès BD

Expressjs n’intègre rien pour l’accès aux données. Il faut prendre un driver, un ORM ou un query builder

Driver natif

ORM (Object-Relational Mapper)

  • gère la persistence à travers des objets JS pour vous,
  • marche out of the box, toute l’interface BD est cachée,
  • peu d’accès aux fonctionnalités spécifiques SGBD,
  • risque de produire des requêtes pathologiques,
  • exemple : https://sequelize.org/

Query builder (une solution intermédiaire)

  • une construction JavaScript des requêtes et du schéma,
  • permet de s’affranchir de SQL,
  • pas de persistance des objets,
  • exemple : https://knexjs.org/, voir demo-knex.mjs

Exemple

Extrait d’un fichier database.mjs

import sqlite3 from "sqlite3";
import { open } from "sqlite";

const databaseFile = process.env.DB_FILE;
const conn = await open({
  filename: databaseFile,
  driver: sqlite3.Database,
});
function getAllLinks() {
  return conn.all("SELECT * FROM link;");
}

function postLink(short, long) {
  return conn.get("INSERT INTO link(short, long) VALUES($short, $long) RETURNING *;", {
    $long: long,
    $short: short,
  });
}

export default {getAllLinks,  postLink, ... };

Démonstration/TODO

Exemple démo avec le premier serveur Web

On donne l’exemple d’un serveur Express :

Et un exemple avec https://knexjs.org/

middleware application utlilisés

express.static("public")
requestTime
setHeaders({ "Warning": "this is sloppy", "X-API-KEY": "deadbeef00" })
debugMiddleware
appErrorHandler

Routes implémentées

Method URI
GET http://localhost:5000/
GET http://localhost:5000/error/:code
GET http://localhost:5000/api
GET http://localhost:5000/api/bogus
GET http://localhost:5000/api/sync-throw
GET http://localhost:5000/api/async-throw
GET http://localhost:5000/api/await/:duration

TP5

Faire la partie 2 du TP5