Serveur web et base de données
Ce TP vous permettra de créer un serveur web avec express, et de le connecter à une base de données SQL.
Nous reprendrons le projet du TP précédent, et nous ajouterons une base de données pour stocker les données des menus et des commandes.
Nous créerons une nouvelle vue pour le restaurateur qui affiche les commandes effectuées.
Objectifs d’apprentissage
- Continuer d’approfondir l’architecture MVC (Modèle-Vue-Contrôleur)
- Utiliser un router avec express
- Refactorer du code en s’appuyant sur des tests
- Utiliser une base de données dans une application web (lecture et écriture)
- Utiliser async/await pour gérer les promesses dans un cas d’utilisation réel
Sommaire
- Préparatifs
- Exercice 1 : refactoring du code
- Exercice 2 : utiliser la base de données en lecture
- Exercice 3 : utiliser la base de données en écriture
- Exercice 4 : créer une page qui liste toutes les commandes
Préparatifs : installer et lancer le projet
Cette fois-ci, nous utiliserons l’environnement de développement sur les machines de l’école.
Des scripts docker ont été créés pour lancer le serveur et la base de données.
Clonage du projet
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 TP-2 https://github.com/johangirod/TP-serveur-web
cd TP-serveur-web
Présentation du projet
Ce projet reprend à la fin de l’exercice 2 du TP précédent. Il contient donc déjà un serveur web avec express, les vues, et les routes permettant d’afficher les menus et de commander.
Le code se trouve dans le dossier src
.
Ouvrez le dossier dans vscode avec la commande code .
. Vous pouvez parcourir le code source pour vous familiariser avec le projet.
Il y a une petite différence dans le fichier index.ts
avec le TP précédent. Pouvez-vous la trouver ?
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
Ce projet utilise une base de données mySQL. Pour créer et lancer un container docker avec la base de données, lancer la commande suivante :
sh scripts/database-start.sh
Pour initialiser la base de données et lancer le serveur, lancer la commande suivante :
sh scripts/start.sh
Vérifiez que le serveur est bien lancé en allant sur http://localhost:3000
.
Lancez les tests et vérifiez que tout est vert avec la commande suivante :
sh scripts/test.sh
A tout moment, vous pouvez voir les logs du serveur avec la commande suivante :
docker logs -f app
Exercice 1 : refactoring du code
Le fichier index.ts
contient tout le code du serveur. Nous allons le découper en plusieurs fichiers pour améliorer la lisibilité et la maintenabilité du code. C’est ce qu’on appelle le refactoring ( « refactorisation » ou « remaniement » en français)
Nous nous baserons sur l’architecture MVC (Modèle-Vue-Contrôleur) pour découper le code.
À chaque étape de l’exercice, vous pourrez lancer les tests pour vérifier que tout fonctionne toujours.
Le refactoring est une étape importante dans le développement d’une application. Il permet de rendre le code plus lisible et plus maintenable.
En effet, plus l’application grandit, plus il devient difficile de comprendre le code et de le modifier si tout est dans un seul fichier.
Utiliser un router
Créer un fichier src/routes.ts
. Nous allons déplacer les routes de l’application dans ce fichier. Il ne restera plus que la configuration du serveur dans index.ts
.
Pour créer un router avec express, nous utiliserons la méthode Router
d’express.
import express from 'express';
const router = express.Router();
On peut ensuite ajouter des routes à ce router avec les méthodes get
, post
, etc, de la même façon qu’avec l’objet app
.
router.get('/', (req, res) => {
// ...
});
Déplacer toutes les routes du fichier index.ts
vers le fichier routes.ts
, en remplaçant app
par router
.
Une route est définie par une méthode HTTP et un chemin. Par exemple, la route GET /
correspond à la page d’accueil, et la route POST /commander
correspond à la page de commande.
Avec Express, on peut définir une route avec les méthodes get
ou post
par exemple (comme vu dans le TP précédent).
Pour utiliser ce router dans l’application, il faut l’ajouter à l’application avec la méthode use
:
import router from './routes';
// <...>
app.use(router);
Définir les contrôleurs
Un contrôleur est la fonction qui est appelée lorsqu’une route est activée. Il prend en paramètre la requête et la réponse, et effectue des actions dans la réponse en fonction de la requête. Il peut, par exemple, récupérer des données dans le modèle, et les afficher dans une vue.
Créer un fichier src/controllers.ts
. Nous allons déplacer les fonctions de callback des routes dans ce fichier.
Ce fichier devra exporter les fonctions suivantes :
getHomePage
getMenusPage
getCommanderPage
createCommandeFromFormulaire
Déplacer les fonctions de callback des routes dans ce fichier, et remplacer les fonctions de callback par les fonctions du controller.
Pour garder la signature des fonctions, il faudra ajouter les types Request
et Response
d’express aux paramètres.
// src/controllers.ts
import { Request, Response } from 'express';
export function getHomePage(req: Request, res: Response) {
// <...>
}
Les tests passent toujours ? Parfait ! Nous pouvons continuer.
Exercice 2 : utiliser la base de données en lecture
Nous allons maintenant faire en sorte que notre application utilise une base de données pour récupérer les informations sur les menus et les commandes.
La base de données est déjà créée et contient les données des menus, ainsi qu’une table vide pour les commandes.
Vous pouvez explorer la base grâce à la commande suivante :
sh scripts/database-cli.sh
Le mot de passe est secret
.
Pour voir la table des menus, lancer la commande suivante :
SELECT * FROM menus;
Vous devriez voir apparaître la liste des menus, avec le nom, la description, le prix, et l’id.
Récuperer les menus dans la base de données
Dans le fichier src/models.ts
, nous allons modifier la fonction getAllMenus
pour qu’elle utilise la base de données plutôt que l’objet menus
.
- Importer le module
mysql2/promise
pour pouvoir utiliser la base de données avec des promesses.
// models.ts
import mysql from 'mysql2/promise';
- Créer une connexion à la base de données avec les informations de connexion stockées dans les variables d’environnement.
// models.ts
const pendingConnection = mysql.createConnection({
host: process.env.MYSQL_HOST,
user: process.env.MYSQL_USER,
database: process.env.MYSQL_DB,
password: process.env.MYSQL_PASSWORD
});
Rendre la fonction
getAllMenus
asynchrone, en ajoutant le mot cléasync
devant la fonction et changeant le type de retour deMenu[]
àPromise<Menu[]>
.Dans la fonction, attendre que la connexion soit établie en utilisant la méthode
await
sur la promesse retournée par la méthodeconnect
et stockée dans la variablependingConnection
.
// function getAllMenus
const connection = await pendingConnection;
- Récupérer les menus dans la base de données avec une commande SQL grâce à la méthode
execute
.
const queryResult = await connection.execute('SELECT * FROM menus');
- Retourner les menus récupérés. Les menus se trouvent dans le premier élément du tableau retourné par la méthode
execute
.
return queryResult[0] as Menu[];
- Modifier le controller
getMenusPage
pour qu’il attende que la fonctiongetAllMenus
soit terminée avant de continuer en ajoutant le mot cléasync
devantfunction
et le mot cléawait
devantgetAllMenus
.
// controller getMenusPage
const menus = await getAllMenus();
Note : il n’y a pas de tests pour cet exercice. Vous pouvez vérifier que tout fonctionne en lançant le serveur et en allant sur la page des menus.
Récupérer un menu par son id
De la même manière, nous allons modifier la fonction getMenuById
pour qu’elle utilise la base de données. La requête SQL pour récupérer un menu par son id est la suivante :
SELECT * FROM menus WHERE id = ?
La fonction execute
remplacera les ?
par les valeurs passées dans un tableau en second paramètre :
connection.execute('SELECT * FROM menus WHERE id = ?', [id]);
Vérifiez que tout fonctionne en cliquant sur « commander » sur la page des menus.
Exercice 3 : utiliser la base de données en écriture
Le but de cet exercice est de sauvegarder les commandes passées par les clients dans la base de données.
Créer un modèle pour une commande
Ajouter une section
commandes
dans le fichiermodels.ts
(comme pour les sectionsmenus
etrestaurant
).Créer un type typescript
Commande
avec les propriétés suivantes :id
: un identifiant unique pour la commandename
: le nom du clientaddress
: l’adresse du clientphone
: le numéro de téléphone du clientmenuId
: l’id du menu commandé
Créer une fonction
createCommande
avec le type suivant :
export function createCommande(
name: string,
address: string,
phone: string,
menuId: string
): Promise<Commande> {
// <...>
}
- Utiliser la méthode
execute
avec une la requête SQL suivante pour insérer une nouvelle commande dans la base de données :
INSERT INTO orders (name, address, phone, menu_id) VALUES (?, ?, ?, ?)
- Retourner la commande créée avec l’id généré par la base de données. Pour cela on peut utiliser la propriété
insertId
retournée parexecute
.
const commandeId = queryResult[0].insertId;
- Modifier le controller
createCommandeFromFormulaire
pour qu’il utilise la fonctioncreateCommande
et qu’il affiche l’id de la commande dans le message de confirmation.
Pour tester, vous pouvez créer une commande avec le formulaire, et vérifier que la commande est bien créée dans la base de données avec la commande `SELECT FROM orders, via le script
scripts/database-cli.sh`.*
Exercice 4 : créer une page qui liste toutes les commandes
Créer une nouvelle page commandes
Créer une nouvelle page /commandes
qui affiche la liste des commandes passées, avec le nom, l’adresse, le téléphone et l’id du menu commandé.
Vous devrez créer une route, un controller, une vue, et une nouvelle fonction dans le modèle.
Afficher le nom du menu commandé avec la commande
On veut afficher le nom du menu commandé plutôt que son id. Pour cela, il faut modifier la fonction getAllCommandes
pour qu’elle récupère le nom du menu avec une jointure SQL.
- Modifier le type
Commande
pour ajouter la propriétémenuName
de typestring
- Modifier la fonction
getAllCommandes
pour qu’elle récupère le nom du menu avec une jointure SQL.SELECT orders.*, menus.name AS menu_name FROM orders JOIN menus ON orders.menu_id = menus.id
Bonus : améliorations
Ajouter des filtres sur la page des commandes
Sur la page des commandes, ajouter des boutons pour filtrer les commandes par menu commandé. Vous devrez :
- créer une nouvelle fonction dans le modèle
getCommandesByMenuId
qui prend en paramètre l’id du menu et retourne la liste des commandes correspondantes. - ajouter un lien par menu dans le fichier
commandes.handlebars
qui pointe vers la page/commandes?menu=ID_DU_MENU
- modifier le controller créer précédement pour qu’il utilise la nouvelle fonction du modèle si l’id du menu est présent dans la requête.
Ajouter un bouton pour supprimer une commande
Ajouter un bouton pour supprimer une commande à côté de la commande. Pour cela, il faudra créer une nouvelle route et un nouveau controller. Cette route aura pour méthode DELETE
et prendra en paramètre l’id de la commande à supprimer : /commandes/:id
.
Ajouter la gestion d’erreur
Que se passe-t-il si la base de données n’est pas disponible ? Modifiez les controllers pour prendre en compte le cas où l’appel au modèle retourne une erreur.