Créer un serveur web en Node

En exploitant le package http, il est facile de créer un mini serveur web. Le serveur est en fait un emitter. On lui passe en paramètre la callback à appliquer quand un client se connecte :

http.js
const http = require('http');

const serveur = http.createServer((request, result) => {
  // on peut récupérer l'url demandée par l'utilisateur
  let url = request.url;

  // pour envoyer des données (HTML?) vers le client :
  result.write(`hello ${url}`);
  result.end();
}).listen(8080); // on indique au serveur d'écouter le port 8080
Le mini serveur web en pratique

Notez que, dans le code du fichier http.js, on peut faire autant de result.write() que l’on souhaite. C’est le result.end() qui terminera la requête (autrement dit, elle correspond au close() que l’on peut retrouver dans d’autres langages comme Python).

Le serveur avec plusieurs pages  

L’idée pour servir de multiples pages consiste simplement à faire des if/else sur la valeur de request.url, comme indiqué ci-dessous. Évidemment, c’est moyennement utilisable pour un gros site.

http2.js
const http = require('http');

const serveur = http.createServer((request, result) => {
  if (request.url === '/toto') {
    result.write('<body>acces a toto</body>');
    result.end();
  }
  else if (request.url === '/toto/titi') {
    result.write('<body>acces a toto/titi<br/>');
    result.write('fin de l\'acces</body>');
    result.end();
  }
  else {
    result.writeHead(404, 'page non trouvee');
    result.end();
  }
}).listen(8080); // on indique au serveur d'écouter le port 8080

Passage à l'échelle du serveur web  

Il y a deux inconvénients majeurs à utiliser le serveur ci-dessus :

  1. Son code est pénible à écrire s’il y a beaucoup de routes différentes ;

  2. C’est peu pratique si l’on a des routes paramétrées.

La solution : utiliser Express

  • C’est un framework puissant et pratique pour gérer les routes.

  • Il permet de réaliser des services (API) RESTfull (REpresentational State Transfer).

  • Il permet de mettre en œuvre les opérations CRUD (Create, Read, Update, Delete).

Les opérations CRUD et leurs méthodes HTTP associées sont les suivantes :

Op. CRUD Signification Méthode HTTP
Create créer de nouvelles données POST
Read récupérer des données GET
Update mise à jour de données PUT
Delete supprimer des données DELETE

Exemple Angular de GET :

http_client1.ts
export class CoursesService {
  constructor(private http : HttpClient) { }

  getCourses() : Observable<Course[]> {
    return this.http.get<Course[]>(
      'http://127.0.0.1/forum/getCourses.php'
    );
  }
}

Exercice 1 : Node - Installations de et pour Express   

Express est disponible via le package npm express. Installez-le.

Vous verrez par la suite qu’Express aura également besoin d’autres packages npm afin de récupérer les données des posts, de gérer les problèmes de cors, etc. Aussi, installez les packages suivants : cors et body-parser.

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 (ci-dessous 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 à accéder à l'URL
// "/toto" via un GET :
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 navigateur
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 ce que vous avez utilisé avec Postman et Angular. C’est également 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

Évidemment, 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, comme vous l’avez fait en PHP. 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 toto.js ci-dessous, qui va répondre à la requête sur la route /toto :

toto.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('./toto');

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

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

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 répertoire contient le code de toutes les requêtes SQL. Ici, il s’agit du fichier sql_connect.js qui réalise la connexion à la base de données et du fichier sql_courses.js qui réalise des requêtes SQL sur la table Courses.

  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 my_route.js.

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

.
├── config.js            # fichier de configuration du backend
├── database             # répertoire contenant toutes les queries SQL
│   ├── sql_connect.js   # script de connexion au serveur SQL
│   └── sql_courses.js   # des queries SQL
├── express_mysql2.js    # le serveur backend
├── my_route.js          # le fichier à exécuter pour la route /myroute
├── package.json
└── package-lock.json
config.js
// étant donné qu'il y aura plein de variables à exporter, autant les
// placer dans un objet Javascript
const config = {
  // la configuration pour se connecter à la base de données
  sqlHost: 'localhost',
  sqlLogin: 'polytech',
  sqlPassword: 'polytech',
  sqlDatabase: 'polytech',
  charset: 'utf8',

  // les noms des tables de la base de données
  sqlCourses: 'forumCourses',
};

// ici, j'indique directement que exports est égal à config. Cela va
// permettre d'écrire directement : const config = require(./config);
module.exports = config;
database/sql_connect.js
// script de connexion à la base de données
const mysql = require('mysql2');
const config = require('../config');

const db = mysql.createConnection({
  host: config.sqlHost,          // ici, on peut utiliser notre fichier
  user: config.sqlLogin,         // de configuration pour entrer les
  password: config.sqlPassword,  // paramètres de connexion
  database: config.sqlDatabase
});
db.connect();

module.exports = db;
database/sql_courses.js
// on peut écrire autant de fois que l'on veut les "require" ci-dessous,
// tout au long de l'application, ils ne seront exécutés et importés
// qu'une seule fois
const config = require('../config');
const db = require('./sql_connect');

// ici, on retourne une Promise
function myQuery() {
  return new Promise((resolve, reject) => {
    db.query(
      `SELECT * FROM ${config.sqlCourses} 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
      });
  });
}

// Si le fichier sql_courses.js contient toutes les queries se rapportant
// à la table courses de la base de données, module.exports doit être un
// objet contenant plusieurs queries, d'où l'export suivant :
module.exports.myQuery = myQuery;
my_route.js
// j'utilise Courses avec un C majuscule car c'est un objet, qui contiendra
// plusieurs queries. Notez qu'en important ce fichier, on assure que l'on
// se connecte à la base de données
const Courses = require('./database/sql_courses');

// 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 Courses.myQuery();
  result.send ('Reponse du serveur: ' + JSON.stringify(rows));
}
module.exports = myRoute;
express_mysql2.js
const express = require ('express');

const app = express ();

// pour gérer les posts
const bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

// 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`); });

Exercice 2 : Backend - Mise en place du serveur Express   

  1. En vous inspirant de ce qui précède, écrivez le fichier qui démarre votre serveur Express sur le port 3000 (appelons-le serveur.js) et créez la route du checkLogin qui, pour l’instant, ne transmettra que la chaîne de caractères « toto ». Testez avec Postman que cela fonctionne bien.

  2. Rajoutez le fichier message.js suivant qui émule les fonctions sendMessage et sendError de PHP :

    message.js
    // renvoie un message au format JSON. On a besoin de passer en paramètre
    // res, la réponse que l'on envoie au client (Angular). Le paramètre
    // data est un objet JavaScript. Globalement, cette fonction est
    // équivalente au "echo json_encode(data);" que vous utilisiez en PHP
    function sendMessage(res, data) {
        res.json({ status: 'ok', data: data });
    }
    
    function sendError(res, reason) {
        res.json({ status: 'error', data: {reason: reason }});
    }
    
    // avec les accolades, il est possible d'exporter plusieurs fonctions sans que
    // celui qui importe le fichier ait besoin de spécifier nom_module.fonction.
    // par exemple, si j'importe message.js, je peux appeler directement sendMessage()
    module.exports = { sendMessage, sendError };
    

    dans ces fonctions, il y a deux différences principales avec celles de PHP auxquelles il faudra faire attention :

    1. Elles nécessitent qu’on leur passe l’objet res. Il n’y a pas moyen d’éviter cela.

    2. En PHP, la dernière instruction de ces fonctions était die, qui permettait de terminer le script dès qu’on les appelait. Ici, on ne peut pas faire la même chose car cela arrêterait le serveur. Ce que je vous conseille donc pour parvenir au même résultat, c’est de ne pas appeler directement sendMessage ou sendError mais plutôt de faire :

      return sendMessage(res, mes_données);
      return sendError(res, "aucune raison valable");
      

      En préfixant avec return, on arrête la fonction qui correspond à la route requêtée par l’utilisateur mais pas le serveur.

    Pour importer ces fonctions, il suffira d’écrire :

    myRoute.js
    const {sendError, sendMessage} = require ("./message");
    
  3. Modifiez votre fichier checkLogin.js de manière à ce qu’il utilise sendMessage pour toujours renvoyer un message indiquant une authentification réussie. Redémarrez votre serveur et testez avec Postman.

  4. C’est pénible de devoir redémarrer le serveur à chaque fois que l’on réalise une modification dans le code. Pour éviter cela, installez le package npm nodemon. À partir de maintenant, plutôt que d’exécuter node serveur.js, exécutez :

    nodemon serveur.js
    

    Chaque fois que vous modifierez des fichiers, nodemon redémarrera automatiquement votre serveur.

Exercice 3 : Forum - checkLogin et base de données   

Modifiez maintenant votre fichier checkLogin.js de manière à ce que vous testiez bien que le client a transmis via post un login et un password. Si, selon votre base de données, le couple login/password est invalide, checkLogin.js renvoie un message d’erreur, sinon un message indiquant que l’utilisateur a bien été authentifié.

Exercice 4 : Forum - Mise à jour du frontend   

Votre frontend Angular doit être mis à jour afin d’utiliser votre nouveau backend en Node/Express. Ainsi, les URL du backend doivent être modifiées. Par exemple, en PHP, le script assurant l’authentification des utilisateurs peut être :

http://127.0.0.1/forum-angular-php/backend/checkLogin.php

alors que celui en Node/Express pourra être :

http://127.0.0.1:3000/checkLogin

De la manière dont vous avez créé votre frontend Angular, seules 2 modifications sont nécessaires pour que celui-ci utilise votre backend en Node/Express. Elles sont situées toutes les deux dans le code de la classe MessageService :

  1. remplacez le préfixe de l’URL du backend PHP par http://127.0.0.1:3000 (en supposant que votre serveur Express écoute le port 3000). Supprimez également l’ajout de l’extension .php que vous rajoutiez aux URLs.

  2. Traditionnellement, dans les transferts d’informations vers Node/Express, on n’utilise pas les FormData, on transmet directement des objets Javascript. Donc, mettez entre commentaires le code que vous aviez écrit pour remplir le FormData et passez directement en argument de this.http.post les données passées en argument à votre méthode sendMessage.

Voilà, la mise à jour de votre frontend est terminée. Vous n’aurez plus aucune modification à y apporter. Notez bien que cela résulte du fait que l’on a utilisé un service de messagerie qui abstrait les envois de messages, plutôt que d’utiliser directement dans toutes les classes des instances de HttpClient.

Exercice 5 : Backend - Test d'interaction avec le frontend   

Testez si, lorsque vous cliquez sur le bouton de connexion de votre frontend, vous êtes bien redirigé(e) vers la page des cours et, seulement ensuite, lisez le paragraphe suivant.

Vous obtenez un message d’erreur dû au CORS. Eh oui, votre backend n’est plus sur le port 80, il est sur le port 3000, mais votre frontend est toujours sur le port 4200, qui est différent de 3000, d’où le CORS. Pour pallier cela, rajoutez dans le fichier serveur.js les instructions suivantes qui vont régler le problème, comme le faisait le début du fichier helper.php :

serveur.js
........

const app = express ();

// permet d'éviter le problème de CORS que l'on avait déjà vu
const cors = require ('cors');
app.use(cors({origin: 'http://127.0.0.1:4200', credentials: true}));

........

Retestez une connexion à partir de votre frontend. Cela devrait fonctionner un peu mieux même si ce n’est pas encore « top » puisque vous n’avez pas encore écrit le fichier getCourses.js.

Indice 1 

Question : Mon checkLogin fonctionnait très bien avec Postman mais pas avec mon frontend. Est-ce normal ?

Réponse : Non. Vérifiez bien que la route que vous avez indiquée dans votre serveur est app.post et non app.get.

Exercice 6 : Backend - La liste des cours   

Rajoutez à votre backend node la route getCourses ainsi que le code permettant de répondre à cette route (vous traduirez simplement le code PHP que vous aviez écrit pour cette route). Pour l’instant, on suppose que l’utilisateur a bien été authentifié et vous rajouterez au début de votre code une variable qui indique « en dur » qui est l’utilisateur.

Testez avec Postman que votre code est correct puis testez avec votre frontend.

Optionnel - quelques lectures

Voici quelques références qui pourraient vous être utiles concernant le CORS :

 
© C.G. 2007 - 2025