Création d'un serveur web avec Express

Pour créer un serveur Express, il y a essentiellement 4 étapes :

  1. importer Express ;

  2. appeler la fonction express() afin de créer une application Express ;

  3. écrire le code permettant de répondre aux requêtes (ici app.get) ;

  4. faire un listen pour que le serveur écoute l’arrivée de nouveaux clients.

Voici un exemple de serveur web en Express :

express1.js
// on importe express (cela renvoie une fonction, que l'on nomme express)
const express = require ('express');

// on appelle la fonction pour créer notre appli Express. Cela permettra d'appeler
// les méthodes app.get(), app.post(), etc., pour réaliser les opérations CRUD
const app = express ();
const port = 8080;

// ici, on indique ce qu'il faut faire si le client demande l'URL `/toto` :
app.get ('/toto', (request,result) => {
    // result étend les méthodes du module Http. Send ne peut être appelé qu'1 fois
    // et il inclut le `end()` que l'on exécute après les `write`.
    result.send('<body>acces a toto</body>');
});

app.get ('/toto/titi', (request,result) => {
   result.write ('<body>acces a toto/titi<br/>');
   result.write ('fin de l\'acces</body>');
   result.end ();
});

// on écoute les clients. Si url non trouvée ci-dessus, Express renvoie une erreur 404
app.listen (port, () => { console.log ('listening'); });

Pour l’exécuter :

node express1.js

Voici le résultat :

Affichage provenant du serveur Express

Spécification explicite du port à écouter :

Dans le code ci-dessus, on imposait dans le code de l’application que le serveur attende ses clients sur le port 8080. On peut faire en sorte que le serveur récupère cette valeur directement de l’environnement dans lequel il est exécuté :

express2.js
const express = require ('express');
const app = express ();

// en principe, pour spécifier le port d'écoute, on récupère la valeur d'une  variable
// d'environnement PORT, si elle existe, sinon on utilise le port que l'on veut
const port = process.env.PORT || 8080;

app.get ('/toto/titi', (request,result) => {
   result.write ('<body>acces a toto/titi<br/>');
   result.write ('fin de l\'acces</body>');
   result.end ();
});

app.listen (port, () => { console.log (`listening du port ${port}`); });

Pour exécuter ce code en spécifiant dans l’environnement le numéro de port :

export PORT=4242
node express2.js

Voici le résultat dans la console :

listening du port 4242

Les routes paramétrées

Il est possible de spécifier des valeurs de paramètres dans les routes servies par le serveur Express. L’exemple ci-dessous montre comment faire. Il suffit d’ajouter un : puis le nom du paramètre dans la route. Le tableau request.params contient alors les valeurs de tous les paramètres de la route.

express3.js
const express = require ('express');
const app = express ();
const port = process.env.PORT || 8080;

// comme dans les routes d'Angular, on peut spécifier des paramètres via ':'
app.get ('/toto/:course_id/topics/:topic_id', (request,result) => {
    // tous les paramètres sont récupérables dans l'objet request.params
    result.send (`
    cours numero ${request.params.course_id}<br/>
    topic numero ${request.params.topic_id}
    `);
});

app.listen (port, () => { console.log (`listening du port ${port}`); });

Voici le résultat obtenu :

Serveur Express avec routes paramétrées

Les paramètres optionnels : query strings

Il est possible de spécifier des paramètres dans la route qui sont optionnels. Voir, dans l’exemple ci-dessous, le paramètre sortBy.

express4.js
const express = require ('express');
const app = express ();
const port = process.env.PORT || 8080;

// Les paramètres spécifiés par ':' sont des paramètres "obligatoires".
// si l'on veut rajouter des paramètres optionnels, on utilise des "query strings" :
// on les spécifie avec ?nom=valeur dans l'URL du browser
app.get ('/toto/:course_id', (request,result) => { // route = paramètres obligatoires
    // les paramètres obligatoires sont récupérables dans l'objet request.params
    // les paramètres optionnels sont dans l'objet request.query
    result.send (`cours numero ${request.params.course_id}<br/>
    tri par ${request.query.sortBy}`);
});

app.listen (port, () => { console.log (`listening du port ${port}`); });

Voici le résultat obtenu :

Serveur Express avec une route contenant un paramètre optionnel

Les posts et leurs données

Comme vous l’avez vu en TP, il existe plusieurs manières de transférer des informations du navigateur vers le serveur web. En autres, on peut le faire dans le body de la requête. C’est la méthode qu’utilise le code ci-dessous, en exploitant le package bodyParser.

express5.js
const express = require ('express');
const app = express ();
const port = process.env.PORT || 8080;

// bodyParser permet de mettre en forme le body du post pour qu'on puisse facilement
// l'utiliser par la suite (c'est lui qui contient les données).
const bodyParser = require('body-parser');

// pour pouvoir parser des data transmises en application/x-www-form-urlencoded (comme
// le font les <form></form>) ainsi que des données transmises en JSON (Ajax) :
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

// ici, on récupère une demande du client transmise par post
app.post ('/toto', (request,result) => {
    // les données des post sont accessibles via request.body
    result.write ('Reponse de notre serveur : ');
    result.write (JSON.stringify(request.body));
    result.end ();
});

app.listen (port, () => { console.log (`listening du port ${port}`); });

Supposons que l’utilisateur ait chargé la page html ci-dessous :

formulaire.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Formulaire Express5</title>
</head>
<body>
  <form method="post" action="http://127.0.0.1:8080/toto">
    champ 1 : <input type="text" name="mon_champ1" /><br/>
    champ 2 : <input type="text" name="mon_champ2" /><br/>
    <button name="mon_bouton" value="bouton">soumettre à Express</button>
  </form>
</body>
</html>

et qu’il ait rempli les champs de la manière suivante :

Le formulaire rempli

alors, il obtient la réponse suivante du serveur express5.js :

La réponse du serveur

Une autre manière de requêter le serveur Express consiste à utiliser Postman :

Accès au serveur Express via postman

Une bonne organisation des fichiers

Evidemment, si le serveur Express gère de nombreuses routes, il peut être fastidieux de placer tout le code correspondant à celles-ci dans le même fichier. De plus, une telle approche rend le serveur difficilement maintenable.

Une méthode plus viable consiste à créer un fichier par route. Dans ce cas, l’idée consiste à placer dans chacun de ces fichiers une fonction qui prend en paramètres le request et le result et qui exécute précisément le code pour répondre à la requête adressée à la route. Par exemple, si l’on regarde le serveur express5.js ci-dessus, on peut créer le fichier express6b.js ci-dessous, qui va répondre à la requête sur la route /toto :

express6b.js
function toto (request,result) {
  // les données des post sont accessibles via request.body
  result.write('Reponse de notre serveur : ');
  result.write(JSON.stringify(request.body));
  result.end();
}
module.exports.toto = toto;

Il suffit maintenant d’importer ce fichier dans le serveur Express et d’indiquer au app.post qu’il faut exécuter la fonction toto() :

express6.js
const express = require ('express');

const app = express ();
const port = process.env.PORT || 8080;

const bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

// on importe ici les scripts des pages. Le mieux : 1 fichier par route
const toto = require('./express6b');

app.post ('/toto',(request, result) => { toto.toto(request,result); });

app.listen (port, () => { console.log (`listening du port ${port}`); });

Comment effectuer des requêtes SQL

Certains des scripts du serveur Express doivent communiquer avec des serveurs SQL afin de pouvoir répondre aux requêtes des utilisateurs. Node possède un package mysql2 prévu à cet effet. Son utilisation est assez similaire à celle de PDO. Pour se connecter, il faut utiliser la fonction createConnection() :

mysql.js
const mysql = require('mysql2');

const db = mysql.createConnection({  // connexion à la BD. Cela produit une
  host     : 'localhost',            // instance qui sera utilisée pour les
  user     : 'polytech',             // queries.
  password : 'polytech',             // A noter que cet objet continuera d'exister
  database : 'polytech'              // jusqu'à ce qu'on applique sa méthode end()
});
db.connect(); // réalisation de la connexion à la base de données

Une fois la connexion réalisée, comme dans PDO, dans les requêtes SQL, on ne fournit pas directement les valeurs de certains paramètres mais plutôt des ?. La différence principale avec PDO, c’est que les requêtes sont asynchrones. Ainsi, la fonction query() prend en arguments 3 paramètres :

  1. la commande SQL à exécuter, mais dans laquelle les valeurs communiquées par l’utilisateur sont remplacées par des ? ;

  2. un tableau contenant les valeurs correspondant aux ?, dans le même ordre que les ? ;

  3. une callback (fonction) qui indique ce qu’il faudra faire quand le serveur SQL aura transmis sa réponse. Cette callback possède 2 arguments : le 1er correspond à l’erreur qui s’est produite (si le serveur SQL renvoie une erreur) et le 2ème contient le résultat de la requête si elle s’est bien passée.

mysql.js
db.query( // on exécute une requête SQL
    'SELECT * FROM courses WHERE id <= ?', // même syntaxe qu'en PDO !
    [2],                                   // ici, ce sont les data de PDO
    (error, results) => {             // les requêtes sont réalisées de manière
      if (error) console.log(error);  // asynchrone => on récupère le résultat via
      else console.log(results);      // une callback, permettant de gérer également
    });                               // les erreurs

Notez que la connexion à la base de données est effective jusqu’à ce qu’on exécute db.end(). Pourquoi est-ce important ? Parce que, comme les db.query() sont asynchrones, tant que vous n’exécutez pas db.end(), votre programme ne peut pas se terminer.

Le programme complet d’interrogation de la base de données est alors :

mysql.js
const mysql = require('mysql2');

const db = mysql.createConnection({  // connexion à la BD. Cela produit une
  host     : 'localhost',            // instance qui sera utilisée pour les
  user     : 'polytech',             // queries.
  password : 'polytech',             // A noter que cet objet continuera d'exister
  database : 'polytech'              // jusqu'à ce qu'on applique sa méthode end()
});
db.connect(); // réalisation de la connexion à la base de données

db.query( // on exécute une requête SQL
    'SELECT * FROM courses WHERE id <= ?', // même syntaxe qu'en PDO !
    [2],                                // ici, ce sont les data de PDO
    (error, results) => {          // les requêtes sont réalisées de manière
      if (error) console.log(error);  // asynchrone => on récupère le résultat via
      else console.log(results);      // une callback, permettant de gérer également
    });                               // les erreurs

console.log('les queries sont asynchrones'); // ceci s'affichera avant les résultats
db.end();  // afin de terminer le programme, il faut arrêter l'attente de l'objet db

Lorsque l’on exécute le script ci-dessus, on obtient un affichage similaire à :

les queries sont asynchrones
[
  { id: 1, nom: 'applis web et mobiles', nbEtuds: 45 },
  { id: 2, nom: 'robotique', nbEtuds: 20 }
]

Notez que le message indiquant que les queries sont asynchrones est écrit avant le tableau contenant les données de la base de données, alors même que l’instruction db.query() a été exécutée avant le console.log, ce qui prouve bien que les requêtes SQL sont asynchrones.

Vers un code apparemment séquentiel :

L’avantage des requêtes asynchrones réside dans le fait qu’elles ne font pas perdre de temps au serveur Express : pendant que l’on attend le résultat d’une requête SQL, Express s’occupe des autres clients qui lui sont connectés. L’inconvénient majeur c’est que, quand on a plusieurs requêtes à exécuter séquentiellement, on a des callbacks imbriquées les unes dans les autres, ce qui devient rapidement pénible à lire.

En Angular, vous avez vu les Observables, qui permettaient, dans une requête asynchrone, de retourner immédiatement un objet, une coquille vide qui, à terme, contiendrait le résultat de la requête. En Javascript, il existe un autre mécanisme relativement similaire : les promesses (Promise en anglais).

L’idée va alors être d’encapsuler la requête dans une promesse et d’inclure le tout dans une fonction. Exécuter cette fonction revient à exécuter la requête. On récupère alors immédiatement une Promise, qui est une coquille vide. Comme pour les Observables, on a un mécanisme qui permet, quand on a la réponse, de l’exploiter : ce n’est plus un subscribe() mais un then. Voici donc une première amélioration du code de mysql.js :

mysql2.js
const mysql = require('mysql2');

const db = mysql.createConnection({ // connexion à la BD. Cela produit une
  host     : 'localhost',                 // instance qui sera utilisée pour les
  user     : 'polytech',                  // queries.
  password : 'polytech',                  // A noter que cet objet continuera d'exister
  database : 'polytech'                   // jusqu'à ce qu'on applique sa méthode end()
});
db.connect(); // réalisation de la connexion à la base de données

function myQuery() {
  // on retourne tout de suite une Promesse, sans attendre la réponse du serveur
  return new Promise((resolve, reject) => {
    db.query(
      'SELECT * FROM courses WHERE id <= ?',
      [2],
    (error, results) => {        // quand on a la réponse du serveur Mysql,
      if (error) reject(error);  // on rejette la promesse en cas d'erreur ou
      else resolve(results);     // on la résout si tout est OK
    });
  });
}

// l'argument du then est une fonction exécutée dès que la promesse est résolue,
// autrement dit, dès que l'on a la réponse du serveur Mysql
myQuery().then((results) => { console.log(results); });
db.end();

Pour l’instant, on n’a pas gagné grand chose car on a juste déplacé le problème d’attente du retour du serveur SQL de db.query à myQuery.then. Cela dit, on a gagné sur un aspect : la fonction myQuery ne mélange pas l’appel au serveur SQL et la logique de mon application (le console.log).

Mais l’avantage d’exploiter les Promises va apparaître quand on va les combiner avec le mot-clef await. Grâce à celui-ci, quand une instruction va récupérer une Promise, Javascript va attendre que la coquille vide de la Promise soit remplie avant de passer à l’instruction suivante. On aura donc, apparemment, un comportement séquentiel. Cela dit, en interne, Node ne va pas bêtement attendre, il va s’occuper des autres clients tant que la Promise n’aura pas été remplie. On aura donc, ainsi, un code qui s’apparente à un code séquentiel mais qui, en réalité, est un code asynchrone.

mysql3.js
const mysql = require('mysql2');

const db = mysql.createConnection({ // connexion à la BD. Cela produit une
  host     : 'localhost',                 // instance qui sera utilisée pour les
  user     : 'polytech',                  // queries.
  password : 'polytech',                  // A noter que cet objet continuera d'exister
  database : 'polytech'                   // jusqu'à ce qu'on applique sa méthode end()
});
db.connect(); // réalisation de la connexion à la base de données

function myQuery() {
  return new Promise((resolve, reject) => {
    db.query(
      'SELECT * FROM courses WHERE id <= ?',
      [2],
      (error, results) => {        // quand on a la réponse du serveur Mysql,
        if (error) reject(error);  // on rejette la promesse en cas d'erreur ou
        else resolve(results);     // on la résout si tout est OK
      });
  });
}
// conserver les avantages de l'asynchronie en ayant un code apparemment séquentiel :
async function getResult() {      // async permet d'utiliser await: await attend la
  const result = await myQuery(); // la résolution de la promesse. Elle prend alors
  console.log(result);            // la valeur passée en argument du "resolve" et la
}                                 // place dans result. Ensuite, seulement, on passe
                                  // à l'instruction console.log
getResult(); // on appelle notre fonction pour interroger la base de données
db.end();

Notez que la fonction getResult() doit être indiquée comme étant asynchrone (mot-clef async) afin de pouvoir utiliser le mot-clef await.

Async : des promesses, toujours des promesses...

Le mot-clef async induit plus de choses qu’on ne pourrait le croire. Dans l’exemple ci-dessous, la fonction myFunc renvoie directement la valeur 3, donc on pourrait légitimement penser que son type de retour est number. En fait, c’est une Promise parce que la fonction est préfixée du mot-clef async :

async.js
async function myFunc () {
  return 3;
}

// async => toute valeur de retour est transformée en promesse
const x = myFunc ();
console.log('x =', x);

async function bof() {
  if (myFunc() === 3) console.log('pas ok');
  if (await myFunc() === 3) console.log('ok');
}
bof();
x = Promise { 3 }
ok

Serveur Express et requêtes SQL

On peut intégrer facilement des requêtes SQL dans les serveurs Express. Pour cela, il suffit de créer la connexion via la fonction createConnection() puis de réaliser les requêtes dans le code exécuté pour chaque route, comme indiqué ci-dessous. Notez que vous n’avez pas intérêt à utiliser db.end() en fin de fichier car vous souhaitez que la connexion à MySQL perdure jusqu’à ce que vous arrêtiez votre serveur Express.

express_mysql.js
const express = require ('express');
const mysql = require('mysql2');

const db = mysql.createConnection({
  host: 'localhost',
  user: 'polytech',
  password: 'polytech',
  database: 'polytech' });
db.connect();
const app = express ();

app.get ('/toto', (req,res) => {
    db.query('SELECT * FROM courses WHERE id <= ?',
    [2],
    (error, rows) => {
      if (error)
          console.log(error);
      else
          res.send (
              'Reponse du serveur: '
              + JSON.stringify(rows));
    });
});

app.listen (3000, () => { console.log (`listening du port 3000`); });
Serveur Express avec requêtes SQL

Réorganisation du code :

Comme on l’a vu plus haut, il est intéressant d’organiser le code avec un fichier par route. Il est également utile de séparer complètement les requêtes SQL de la logique de l’application. Cela permet une maintenance plus facile et, si on décide de changer la base de données, on n’a pas à réécrire tout le serveur mais juste la partie relative à la base de données.

On peut donc réorganiser le code du serveur ci-dessus de la manière suivante :

  1. un fichier ou un répertoire contient le code de toutes les requêtes SQL. Ici, il s’agit du fichier mysqlQueries.js.

  2. Pour chaque route, on a un fichier qui contient les instructions pour répondre à la requête web de cette route. Ici, il s’agit du fichier myRoute.js.

  3. Un fichier contient notre serveur Express. Ici, il s’agit du fichier express_mysql2.js.

mysqlQueries.js
const mysql = require('mysql2');

const db = mysql.createConnection({
  host: 'localhost',
  user: 'polytech',
  password: 'polytech',
  database: 'polytech'
});
db.connect();

// ici, on retourne une Promise
function myQuery() {
  return new Promise((resolve, reject) => {
    db.query(
      'SELECT * FROM courses WHERE id <= ?',
      [2],
      (error, results) => {  // quand on a la réponse du serveur Mysql,
        if (error) reject(error);  // on rejette la promesse en cas d'erreur ou
        else resolve(results);     // on la résout si tout est OK
      });
  });
}
module.exports = myQuery;
myRoute.js
const myQuery = require('./myQueries');

// la fonction exécutée pour la route /myRoute : elle appelle une fonction
// qui requête la base de données
async function myRoute(request, result) {
  const rows = await myQuery();
  result.send ('Reponse du serveur: ' + JSON.stringify(rows));
}
module.exports = myRoute;
express_mysql2.js
const express = require ('express');

const app = express ();

// organisation : 1 fichier par route
const myRoute = require('./myRoute');
app.get ('/myroute', (req,res) => {myRoute(req, res);});

app.listen (3000, () => { console.log (`listening du port 3000`); });
 
© C.G. 2007 - 2024