Tutoriel HTTP/Express Node.js
Ce tutorial est inspiré de How To Create a Web Server in Node.js with the HTTP Module et compléter avec une partie sur Express.
Ce tutorial vous fait prendre en main l’environnement Node.js avec un petit projet de serveur web monté pas à pas, utilisant essentiellement les bilbiothèques standards de Node.js. Le framework http://expressjs.com/ sera introduit ensuite.
- installer Node.js https://nodejs.org/en/download/ pour votre environnement
- cloner le projet de départ de la GitHub Classroom https://classroom.github.com/a/8mQFHDdO
- on obtient un dossier
tutoriel-http-express-node-LOGINqu’on appelleradevweb-tp5par la suite par commodité
- on obtient un dossier
RENDU vous devrez remplir le fichier README.md avec les questions du sujet et commiter/pousser sur GitHub Classroom.
Les différentes étapes à réaliser seront aussi committées. La date limite de rendu est le lundi 29 août 2022 23h59.
Partie 1 : serveur HTTP natif Node.js
Installation
Exécuter la commande npm init dans le dossier devweb-tp5.
Répondre avec les valeurs par défaut, sauf pour entry point: (index.js) où donner la valeur server-http.mjs
À ce stade, un fichier package.json a du être créé à peu près comme suit.
{
"name": "devweb-tp5",
"version": "1.0.0",
"description": "",
"main": "server-http.mjs",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
Toujours dans le dossier devweb-tp5 , créer le fichier server-http.mjs avec le contenu suivant :
import http from "node:http";
const host = "localhost";
const port = 8000;
function requestListener(_request, response) {
response.writeHead(200);
response.end("<html><h1>My first server!<h1></html>");
}
const server = http.createServer(requestListener);
server.listen(port, host, () => {
console.log(`Server is running on http://${host}:${port}`);
});
Enfin, exécuter la commande node server-http.mjs et vérifier que votre application web fonctionne en vous connectant avec votre navigateur.
Question 1.1 donner la liste des en-têtes de la réponse HTTP du serveur.
Servir différents types de contenus
Maintenant, remplacer la fonction requestListener() par la suivante et tester :
function requestListener(_request, response) {
response.setHeader("Content-Type", "application/json");
response.end(JSON.stringify({ message: "I'm OK" }));
}
Question 1.2 donner la liste des en-têtes qui ont changé depuis la version précédente.
Remplacer enfin la fonction requestListener() par la suivante et tester :
import fs from "node:fs/promises";
function requestListener(_request, response) {
fs.readFile("index.html", "utf8")
.then((contents) => {
response.setHeader("Content-Type", "text/html");
response.writeHead(200);
return response.end(contents);
})
.catch((error) => console.error(error));
}
Question 1.3 que contient la réponse reçue par le client ?
Question 1.4 quelle est l’erreur affichée dans la console ? Retrouver sur https://nodejs.org/api le code d’erreur affiché.
Modifier la fonction requestListener() précédente pour que le client recoive une erreur 500 si index.html est introuvable en remplacant le callback de la méthode Promise.catch().
Maintenant, renommer le fichier __index.html en index.html et tester à nouveau.
Enfin, reprenez requestListener() dans le style async/await.
Question 1.5 donner le code de requestListener() modifié avec gestion d’erreur en async/await.
Commit/push dans votre dépot Git.
Mode développement
Dans le dossier devweb-tp5 exécuter les commandes suivantes :
npm install cross-env --savenpm install nodemon --save-dev
Question 1.6 indiquer ce que cette commande a modifié dans votre projet.
Ensuite, remplacer la propriété "scripts" du fichier package.json par la suivante :
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"http-dev": "cross-env NODE_ENV=development nodemon server-http.mjs",
"http-prod": "cross-env NODE_ENV=production node server-http.mjs"
},
Exécuter npm run http-dev, visiter le site, puis pendant que le serveur s’exécute modifier le fichier server-http.mjs en ajoutant la ligne console.log("NODE_ENV =", process.env.NODE_ENV);.
Enregistrer le fichier et vérifier qu’il y a eu rechargement automatique grâce à https://nodemon.io/.
Ensuite, faire la même chose avec la commande npm run http-prod.
Question 1.7 quelles sont les différences entre les scripts http-dev et http-prod ?
Les fichiers .eslintrc.json et .prettierrc sont fournis dans le dossier devweb-tp5. Exécuter la commande suivante pour installe les dépendances :
npm install --save-dev prettier eslint eslint-config-prettier eslint-plugin-import eslint-plugin-jest eslint-plugin-node eslint-plugin-promise eslint-plugin-security eslint-plugin-unicorn
Vérifier que l’autoformattage avec https://prettier.io et le linting avec https://eslint.org/ fonctionnent dans VSCode et en ligne de commande avec les commandes suivantes :
npx eslint server-http.mjsnpx prettier server-http.mjs --write
Commit/push dans votre dépot Git.
Gestion manuelle des routes
Remplacer la fonction requestListener() par la suivante :
async function requestListener(request, response) {
response.setHeader("Content-Type", "text/html");
try {
const contents = await fs.readFile("index.html", "utf8");
switch (request.url) {
case "/index.html":
response.writeHead(200);
return response.end(contents);
case "/random.html":
response.writeHead(200);
return response.end(`<html><p>${Math.floor(100 * Math.random())}</p></html>`);
default:
response.writeHead(404);
return response.end(`<html><p>404: NOT FOUND</p></html>`);
}
} catch (error) {
console.error(error);
response.writeHead(500);
return response.end(`<html><p>500: INTERNAL SERVER ERROR</p></html>`);
}
}
Tester les routes suivantes :
http://localhost:8000/index.htmlhttp://localhost:8000/random.htmlhttp://localhost:8000/http://localhost:8000/dont-exist
Question 1.8 donner les codes HTTP reçus par votre navigateur pour chacune des quatre pages précédentes.
Maintenant, on veut ajouter une route /random/:nb où :nb est un paramètre entier avec le nombre d’entiers à générer. Ajouter cette route au switch et reprendre la page random.html pour générer autant de nombres qu’indiqué dans l’URL.
Pour cela, utiliser request.url.split("/"); qui va décomposer le chemin demandé et faire le switch sur le premier niveau de l’arborescence. Faites en sorte que le switch traite /index.html et / de la même façon.
Commit/push dans votre dépot Git.
Partie 2 : framework Express
On voit que la gestion manuelle des routes avec un grand switch va devenir complexe et laborieuse.
Les frameworks serveur comme http://expressjs.com/, https://koajs.com/, https://www.fastify.io/ ou https://hapi.dev/ vont s’occuper de cette plomberie et proposer une API pour enregistrer des handlers aux différentes routes de l’application.
Création du serveur
Créer le fichier server-express.mjs et exécuter la commande suivante :
npm install --save express http-errors loglevel morgan
Question 2.1 donner les URL des documentations de chacun des modules installés par la commande précédente.
Ensuite, sur le modèle des scripts http-prod et http-dev du fichier package.json, créer les scripts express-prod et express-dev.
Ensuite, ajouter le contenu suivant au fichier server-express.mjs
import express from "express";
import morgan from "morgan";
const host = "localhost";
const port = 8000;
const app = express();
app.get(["/", "/index.html"], async function (request, response, next) {
response.sendFile("index.html", { root: "./" });
});
app.get("/random/:nb", async function (request, response, next) {
const length = request.params.nb;
const contents = Array.from({ length })
.map((_) => `<li>${Math.floor(100 * Math.random())}</li>`)
.join("\n");
return response.send(`<html><ul>${contents}</ul></html>`);
});
app.listen(port, host);
Question 2.2 vérifier que les trois routes fonctionnent.
Question 2.3 lister les en-têtes des réponses fournies par Express. Lesquelles sont nouvelles par rapport au serveur HTTP ?
Remplacer la dernière ligne de server-express.mjs par les suivantes
const server = app.listen(port, host);
server.on("listening", () =>
console.info(
`HTTP listening on http://${server.address().address}:${server.address().port} with mode '${process.env.NODE_ENV}'`,
),
);
console.info(`File ${import.meta.url} executed.`);
Question 2.4 quand l’événement listening est-il déclenché ?
Commit/push dans votre dépot Git.
Ajout de middlewares
Ici, la route de la page d’accueil charge dynamiquement à chaque requête une ressource statique. Ce n’est pas très performant, d’autant plus qu’un middleware Epxress existe déjà pour ça.
- créer un sous-dossier
static - déplacer le fichier
index.htmldans le sous-dossierstatic - extraire l’élément
<style>deindex.htmldans un nouveau fichierstyle.cssque vous lierez àindex.htmlavec<link rel="stylesheet" href="style.css"> - remplacer la route de la page d’accueil par
app.use(express.static("static"));
Question 2.5 indiquer quelle est l’option (activée par défaut) qui redirige / vers /index.html ?
Question 2.6 visiter la page d’accueil puis rafraichir (Ctrl+R) et ensuite forcer le rafraichissement (Ctrl+Shift+R). Quels sont les codes HTTP sur le fichier style.css ? Justifier.
Ajouter la ligne if (app.get("env") === "development") app.use(morgan("dev")); au bon endroit dans server-express.mjs pour activer le middleware Morgan.
Commit/push dans votre dépot Git.
Rendu avec EJS
Le moteur de templating https://ejs.co/ est l’équivalent de Jinja utilisé pour Python/Flask dans l’écosytème Nodes.js/Express. Une extension VSCode est disponible pour EJS.
On va utiliser le moteur EJS pour améliorer la page random générée dynamiquement côté serveur.
- créer un sous-dossier
views/et créer un fichierviews/random.ejsavec le contenu ci-après; - exécuter la commande
npm install --save ejs; - ajouter la ligne
app.set("view engine", "ejs");àserver-express.mjs; - modifier le handler de la route
/random/:nbavecresponse.render("random", {numbers, welcome});pour appeller le moteur de rendu, oùnumbersest un tableau de nombres aléatoires (comme précédemment) etwelcomeune chaîne de caractères.
Contenu de views/random.ejs
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/normalize.css@8.0.1/normalize.css" />
<link rel="stylesheet" href="/style.css" />
<title>Tutorial</title>
</head>
<body>
<div class="center">
<h1><%= welcome %></h1>
<% numbers.forEach(element => { %>
<code><%= element %></code>
<% }) %>
</div>
</body>
</html>
Commit/push dans votre dépot Git.
Gestion d’erreurs
On va maintenant vérifier que le paramètre /random/:nb est bien un nombre. Si ce n’est pas le cas, il faut retourner une erreur HTTP 400.
Pour cela, utiliser le module https://github.com/jshttp/http-errors
- ajouter le module
http-errorsavecnpm - ajouter le
import ... from ...correspondant dansserver-express.mjs - dans la toute
/random/:nb, faites la vérification avecconst length = Number.parseInt(request.params.nb, 10);puisNumber.isNaN(length), si le paramètre, n’est pas un nombre, produire une erreur 400 avecnext(createError(400));
Commit/push dans votre dépot Git.
Avec cette solution, l’erreur n’est pas bien rendue sur le client car elle passe dans le handler d’erreur par défaut d’Express. De plus, quand on visite une page qui n’existe pas, par exemple http://localhost:8000/javascript, la 404 n’est pas terrible non plus.
Ajouter, tout à la fin des routes, juste avant app.listen(port, host);, deux nouvaux handlers comme suit :
app.use((request, response, next) => {
concole.debug(`default route handler : ${request.url}`);
return next(createError(404));
});
app.use((error, _request, response, _next) => {
concole.debug(`default error handler: ${error}`);
const status = error.status ?? 500;
const stack = app.get("env") === "development" ? error.stack : "";
const result = { code: status, message: error.message, stack };
return response.render("error", result);
});
Ensuite, créer, sur le modèle de random.ejs, une vue error.ejs dont le corps est comme suit :
<body>
<div class="center">
<h1>Error <%= code %></h1>
<p><%= message %></p>
</div>
<% if (stack != null) { %>
<pre><%= stack %></pre>
<% } %>
</body>
Question 2.7 vérifier que l’affichage change bien entre le mode production et le mode development.
Commit/push dans votre dépot Git.
Enfin, chargez le module loglevel avec import logger from "loglevel"; puis fixer un niveau de verbosité avec logger.setLevel(logger.levels.DEBUG);.
Remplacez tous les console.log() et variantes par logger.error(), logger.warn(), logger.info(), logger.debug() ou logger.trace() approprié.
Modifier le niveau de verbosité, par exemple logger.setLevel(logger.levels.WARN); et vérifier l’affichage.
Commit/push dans votre dépot Git.
Conclusion
À ce stade du tutoriel, vous avez vues les principales étapes de la création d’une application Node.js/Express. Ces étapes seront déjà réalisées dans le projet de départ du TP6.