Avec corrigé

Serveur web : principes de base

Technologie web 3

Johan Girod

Qu'est-ce qu'un serveur web ?

  • Un programme informatique accessible depuis internet
  • Qui répond à des requêtes de clients
  • En leur renvoyant des ressources web

Qu'est-ce qu'un serveur web ?

Schéma d'une requête HTTP

Accessible depuis internet

  • Utilise un protocole transport : TCP
  • Écoute sur un port réseau prédéfini: 80 (HTTP) ou 443 (HTTPS)
  • Utilise le protocole applicatif HTTP pour la transmission de ressource

La requête HTTP

Une requête est un message texte contenant

  • Un verbe d'action
  • L'adresse d'une ressource à récupérer
  • La version du protocole utilisé
  • Des entêtes (headers) optionnels

			GET /restaurants HTTP/1.0
			Host: example.com
			User-Agent: Mozilla/4.0
			Accept-Language: fr
		

La réponse HTTP

Une réponse est un message texte contenant

  • Un code de statut
  • Des entêtes (headers) optionnels
  • Le corps de la réponse

				
		

Les framework web

Facilitent la création de serveurs web en permettant de manipuler facilement :

  • Les routes et leur paramètres
  • Les requêtes (lecture des entêtes, corps, session, cookies)
  • Les réponses (code de statut, entêtes, corps, type-mime, etc.)

ExpressJS

Le framework web le plus populaire pour Node.js

Principe de base


			
		

TP : Créer un serveur web

TP ExpressJS

Ce TP vous permettra de vous familiariser avec un framework web (expressjs), en créant un site web pour un restaurant. Ce site contiendra une page d’accueil, une page pour afficher les menus, et une page pour commander un menu.

Objectifs

  • Avoir une première expérience avec l’architecture MVC
  • Savoir créer un serveur web avec Express et créer des routes
  • Savoir lancer des tests et interpréter les erreurs
  • Servir les fichiers statiques
  • Utiliser un moteur de template pour générer des pages HTML à partir de vues
  • Comprendre l’utilité du layout
  • Comprendre et utiliser les query string parameters
  • Savoir traiter un formulaire HTML

Sommaire

Préparatifs : installer et lancer le projet

Prérequis : installation

Pour les besoins de développement, nous allons installer nodeJS en local.

Si cela ne fonctionne pas, nous utiliserons une version github codespace, un vscode en ligne avec nodeJS déjà installé.

Installation

Le code se trouve sur github. Pour installer le projet, lancer les commandes suivantes :

# Clone le projet git dans le dossier courant
git clone --branch start https://github.com/johangirod/TP-express
cd TP-express
npm install

Présentation du projet

Ce projet se compose d’un fichier index.ts contenant le code source de votre serveur, et d’un dossier test contenant les tests (vous n’aurez pas à modifier ce dernier).

Le fichier tsconfig.json contient la configuration typescript.

Eslint

Ce projet utilise eslint pour vérifier la syntaxe du code. Pour lancer la vérification, lancer la commande npx eslint ./.

Le fichier .eslintrc.js contient la configuration d’eslint.

Prettier

Ce projet utilise prettier pour formater automatiquement le code. Pour lancer le formatage, lancer la commande npx prettier ./ --write.

Le fichier .prettierrc.js contient la configuration de prettier (vide = standard).

Installer les extensions vscode

Pour profiter des annotations eslint et du formattage automatique de prettier dans vscode, il vous suffit d’installer les extensions recommandées du projet. Pour cela, ouvrez le menu des extensions (Ctrl+Maj+X) et cherchez “@recommended”. Vous devriez voir apparaître les extensions suivantes :

  • ESLint
  • Prettier - Code formatter

Lancer le projet

  1. Créez le fichier index.ts à la racine du projet, contenant un simple console.log("hello world")
  2. Lancez le projet avec la commande npx ts-node-dev index.ts. Vous devriez voir apparaitre « hello world » dans la console.

ts-node-dev est un outil qui permet d’exécuter un fichier node en typescript. Il est très pratique, car il relance automatiquement le serveur à chaque modification du code source.

Tests

Tout au long du TP, vous pourrez vérifier si vous avez réussi l’exercice en lançant les tests avec la commande npm test.

Lancez la commande et vérifiez que tout est vert.

Exercice 1 : un serveur web static

Pour commencer, allez sur la branche exercice-1 du projet :

git checkout -f exercice-1

Le fichier index.html contient la page d’accueil du site. Nous allons maintenant créer un serveur web qui servira cette page.

Ma première route

Nous allons créer une route /ping qui renverra un statut 200.

Codes de réponse HTTP

Le statut HTTP est un code numérique qui est renvoyé par le serveur pour indiquer le résultat de la requête au client. Le code 200 signifie que la requête a été traitée avec succès. Le code 404 signifie que la ressource demandée n’a pas été trouvée. Nous verrons d’autres codes de statut HTTP plus tard dans ce TP.

En savoir plus

Il vous faudra importer la bibliothèque express, créer une instance de l’application, et ajouter une route /ping qui renvoie un statut 200.

  1. Installez express avec la commande npm install express
  2. Installez les types typescript de express avec la commande npm install -D @types/express
  3. Importez express dans le fichier index.ts avec import express from 'express'
  4. Créez une instance de l’application express avec const app = express()
  5. Ajoutez une route /ping qui renvoie un statut 200 avec la méthode app.get
  6. Faire en sorte que le serveur écoute sur le port 3000 avec la méthode app.listen
  7. Lancez le serveur avec npx ts-node-dev index.ts
  8. Lancez les tests avec npm test
  9. Optionnel : ouvrez votre navigateur et allez sur http://localhost:3000/ping
Corrigé

Servir la page d’accueil

Nous allons maintenant servir la page index.html lorsque l’utilisateur se rend sur la page d’accueil du site. Pour cela, vous créerez une route / qui renvoie le fichier index.html.

Pour cela, vous utiliserez la méthode sendFile de l’objet res.

Pour construire le chemin du fichier, vous utiliserez la méthode join du module path. Et pour récupérer le chemin absolu du dossier racine, vous utiliserez la variable __dirname.

Corrigé

Servir les fichiers statiques (images, css, etc.)

Tous les fichiers statiques (images, css, etc.) se trouvent dans le dossier assets. Nous allons maintenant servir ces fichiers statiques, en montant le dossier assets sur le chemin /assets.

Pour cela vous utiliserez la méthode express.static.

Corrigé

Tester le serveur

Vérifiez que tout fonctionne en lançant les tests avec npm test avant de passer à l’exercice suivant.

Exercice 2 : Utiliser un moteur de template

Nous allons à présent utiliser un moteur de template pour générer les pages HTML à partir des données de l’application. Il en existe plusieurs, nous utiliserons handlebars.

Pour commencer, allez sur la branche exercice-2 du projet :

git checkout -f exercice-2

Un nouveau dossier models a été créé, contenant un fichier restaurant.ts qui contient les données du restaurant. Nous allons utiliser ces données pour générer les pages du site qui seront situées dans le dossier views.

Moteur de template

Un moteur de template est un outil qui permet de générer des pages HTML à partir de données. Il permet de séparer le code HTML du code JavaScript, et de générer des pages HTML dynamiques. C’est un outil utilisé dans les applications web qui suivent le pattern MVC.

Mettre en place handlebars

  1. Renommez le fichier index.html en home.handlebars et déplacez-le dans un nouveau dossier views
  2. Installez le moteur de template handlebars pour express express-handlebars en suivant les instructions de la documentation
  3. Vérifiez que les tests de l’exercice 1 passent toujours grâce à la commande npm test exercice-1. Si ce n’est pas le cas, corrigez le code.
Corrigé

Changer le nom et la description du restaurant

  1. Dans le fichier home.handlebars, remplacez le nom du restaurant par {{name}} et la description par {{description}}
  2. Dans le fichier index.ts, importez le fichier restaurant.ts avec import restaurant from "./models/restaurant"
  3. Passez les données du restaurant au moteur de template via res.render
Corrigé

Utiliser les layouts

Un layout est un fichier qui contient le squelette de la page. Il permet de factoriser le code HTML commun à toutes les pages du site, comme le header et le footer. Nous allons mettre à jour le layout par défaut main.handlebars, et laisser la possibilité de paramétrer le titre de la page.

  1. Déplacez le code HTML commun dans le fichier main.handlebars de telle sorte que le fichier home.handlebars ne contienne que le contenu spécifique à la page d’accueil (ce qui se trouve à l’intérieur de la balise <div class="row ...">).
  2. Changez le titre de la page d’accueil (balise <title>) par <nom du restaurant>
  3. Faire en sorte que les tests « Exercice 1 - Home » passent
Corrigé

Créer une page pour afficher le menu

Voici le template bootstrap proposé pour un menu :

<div class="col" data-test-menu>
  <div class="card h-100" role="list-item">
    <div class="card-body">
      <h2 class="card-title">{{name}}</h5>
      <p class="card-text">{{description}}</p>
      <p class="card-text">{{price}}€</p>
    </div>
  </div>
</div>

Voici le container pour afficher les menus :

<div class="row row-cols-1 row-cols-md-3 g-4">
	<!-- Ici, on affichera les menus -->
</div>

Créez une nouvelle page menu.handlebars, servi sur l’URL /menus qui affiche les menus du restaurant présents dans models/menus. Pour éviter de dupliquer le code HTML, vous le bloc handlebars {{#each}} pour afficher tous les menus. Cette page aura un titre <h1> « Menus » et pour titre de page (balise <title>) Menus - <nom du restaurant>

  1. Creer la vue menu.handlebars dans le dossier views
  2. Servir cette page sur le route /menus grâce à app.get et res.render
  3. Passer la variable menus au moteur de template comme argument de res.render
  4. Utiliser {{#each}} dans la vue pour afficher tous les menus
  5. Modifier le titre de la page pour qu’il soit dynamique Menus - <nom du restaurant> (<nom du restaurant> vient du fichier restaurant.ts)
Corrigé

Créer une page pour commander un menu

Nous allons ajouter un bouton sur les menus pour pouvoir les commander. Ce bouton doit rediriger vers la page /commander. Pour passer au serveur l’identifiant du menu, nous allons utiliser un paramètre de l’URL (appelé query string parameter). Par exemple, pour le menu avec l’identifiant xid327y, l’URL sera /commander?menu=xid327y.

Voici le code du bouton :

<!-- contenu du menu -->
<!-- ... -->

<div class='p-3'>
	<a href='/commander?menu={{id}}' class='btn btn-secondary'>Commander</a>
</div>

Query string

Un query string est une chaîne de caractères qui suit le chemin de l’URL et qui contient des paramètres. Il est séparé du chemin par un point d’interrogation ?, et chaque paramètre est séparé par un &. Par exemple, l’URL https://example.com/path?param1=value1&param2=value2 contient deux paramètres : param1 et param2.

Il est possible de récuperer les paramètres d’une requête HTTP avec la propriété query de l’objet req.

  1. Ajouter un bouton « Commander » sur chaque menu qui redirige vers la page /commander avec l’identifiant du menu en paramètre
  2. Créer une page commande.handlebars qui le nom du menu commandé. Cette page aura un titre <h1> « Votre commande : menu » et pour titre de page (balise <title>) Commander - <nom du restaurant>
  3. Servez cette vue sur la route /commander grâce à app.get et res.render
  4. Pour récupérer l’id passé en paramètre de requête, vous pourrez utiliser req.query
  5. Rechercher dans l’array menu celui qui correspond à l’id en entrée
  6. Passer le nom du menu concerné à la vue, comme paramètre de res.render

Aide : vous pourrez utiliser la méthode Array.find pour retrouver le bon menu à partir de l’identifiant

Corrigé

Avant de passer à l’exercice suivant, vérifiez que les tests passent avec npm test.

Exercice 3 : traiter un formulaire HTTP

Nous allons maintenant créer un formulaire pour commander un menu. Ce formulaire contiendra un champ pour le nom, un champ pour l’adresse, et un champ pour le numéro de téléphone. Lorsque l’utilisateur soumettra le formulaire, un message de confirmation s’affichera, lui rappelant ses informations saisies.

Créer le formulaire

  1. Sur la page commander.handlebars, créez un formulaire avec la balise <form> et des balises <input> qui contient les champs suivants :

    • un champ name de type text avec le label « Nom »
    • un champ address de type text avec le label « Adresse »
    • un champ phone de type tel avec le label « Téléphone »
    • un bouton « Commander » de type submit
  2. Créez une route /commander en post qui extrait les données du formulaire et les fourni à la vue commander.handlebars. Vous utiliserez app.post. Pour récupérer automatiquement les données du formulaire, il faudra ajouter un middleware spécifique : app.use(express.urlencoded())

  3. Vérifiez que la route est bien appelée lorsque vous soumettez le formulaire. Pour cela, vous pouvez ajouter un console.log et vérifier si il apparaît dans le terminal.

  4. Modifier la vue commander.handlebars pour ajouter un message de confirmation lorsque les données du formulaire sont présentes. Ce message doit contenir le nom, l’adresse et le téléphone du client.

  5. Veillez à ce que la page continue d’afficher le nom du menu commandé

Astuce : vous pourriez avoir besoin de <input type="hidden"> et de <form method="post">

POST vs GET

Il existe plusieurs méthodes HTTP, permettant d’indiquer une intention au serveur. Les deux plus courantes sont GET et POST.

  • La méthode GET est utilisée pour récupérer des données. Lorsqu’on saisit une URL dans le navigateur, ce dernier utilise la méthode GET.
  • La méthode POST est utilisée pour envoyer des données. Elle signifie que l’on souhaite effectuer une action qui va modifier un état sur le serveur.

Les données de la méthode POST sont envoyées dans le corps de la requête HTTP, et non dans l’URL. Cela permet de transmettre des données plus importantes, comme des fichiers. Pour récupérer les données postées avec un formulaire, on utilise un middleware express appelé express.urlencoded() qui les transforme en objet JavaScript et les ajoute à l’objet req.body.


Corrigé

Exercice 4 : ajouter un middleware pour gérer les erreurs serveurs

Middleware

Un middleware est une fonction de callback qui prend en paramètre un objet req, un objet res, et une fonction next. Il est utilisé pour effectuer des opérations communes à plusieurs routes, comme la vérification des autorisations, la gestion des erreurs, ou le formatage des données.

app.use((req, res, next) => {
	// Ce code est appelé avant chaque route handler (get, post, etc.)
	console.log('Entrée dans le middleware');

	// L'appelle à `next()` permet de passer la main au middleware suivant, ou au route handler si il n'y a plus de middleware
	next();

	// Ce code est appelé après chaque route.
	// On peut accéder à la réponse de la route avec res
	console.log('Sortie du middleware');
});

A noter : l’ordre des middleware est important. Ils sont exécutés dans l’ordre où ils sont déclarés. Par ailleurs, ils doivent être déclarés avant les routes qui les utilisent.

Voir le guide express sur les middlewares

Gérer les erreurs 404

  1. Créer une vue erreur.handlebars qui affiche un message « Page non trouvée » lorsque le code erreur est 404.
  2. Implémenter un middleware express qui affiche la vue avec les bons paramètres lorsqu’une page n’est pas trouvée

Gérer les erreurs 500

  1. Modifier la vue erreur.handlebars pour afficher le message « Erreur serveur » lorsque le code erreur est 500.
  2. Implémenter un middleware express qui affiche la vue avec les bons paramètres lorsqu’une erreur se produit
Corrigé

Exercice 5 : améliorations (bonus)

Post/Redirect/Get

Après avoir saisi la commande, on reste sur la même url, ce qui signifie que si l’utilisateur rafraichit la page, le formulaire est renvoyé. Pour éviter cela, on peut utiliser le pattern Post/Redirect/Get : après avoir traité le formulaire, on redirige l’utilisateur vers une autre page. Il ne pourra donc pas renvoyer le formulaire en rafraichissant la page.

Utiliser un paramètre de l’URL

Plutôt que d’utiliser un query string pour passer l’identifiant du menu, on peut utiliser un paramètre de l’URL. Cela éviterait d’avoir à se servir de input hidden. Par exemple, l’URL /commander/1 permet de passer l’identifiant du menu 1. Pour cela, il faut utiliser un paramètre de l’UgwsRL, et non un query string. On peut utiliser la méthode app.param pour récupérer le menu à partir de l’identifiant, et le passer à la vue.

Refacto

Tout le code source est dans le fichier index.ts. Il serait préférable de séparer le code en plusieurs fichiers, par exemple un fichier controllers.ts qui contient les routes et laisser le fichier index.ts pour la configuration du serveur.

Validation du formulaire

Lors de la soumission du formulaire, on pourrait vérifier que les champs sont bien remplis, et afficher un message d’erreur sur le champs concerné si ce n’est pas le cas. Par ailleurs, on pourrait vérifier que le numéro de téléphone est bien un numéro de téléphone valide.