Express.js est un framework minimaliste pour node.js. Il permet de créer facilement une application web. Son côté minimaliste le rend peu pratique pour créer des application de taille un importante. Nous allons nous en servir pour développer des applications jouant un rôle de serveur de données pour nos applications React.js
node.js est une plateforme logicielle (runtime environment) construite sur l’interpréteur JavaScript V8.
Express est un framework JavaScript minimaliste qui permet d’accélérer le développement d’un serveur node.js. En offrant de nombreuses de fonctionnalités, il simplifie ainsi l’écriture du serveur. En particulier, il permet de gérer aisément le routage et offre de nombreuses fonctionnalités supplémentaires à l’objet HTTP utilisé avec node.js.
mkdir server
cd server
npm init -y
npm install express
touch server.js
npm install express
ajoute le package express à votre application.dependencies
du fichier package.json.Copiez le code suivant dans votre fichier server.js :
const express = require('express');
const app = express();
const port = process.env.PORT || 8000;
app.get('/', (req, res) => {
res.set('Content-Type', 'text/html');
res.send('Hello world !!');
});
app.listen(port, () => {
console.log('Server app listening on port ' + port);
});
Lancez le serveur : node server.js
. Entrez l’URL suivante dans votre navigateur web :
http://localhost:8000
Le message “Hello world!!” apparaît : votre serveur fonctionne.
Commentaires :
http://localhost:8000/xx
Le message “cannot GET /xx” apparaît, indicant que votre serveur ne reconnait pas votre requête.
Le protocole HTTP définit le mode de communication entre le client (votre navigateur) et le serveur (votre application Express). Il définit le format des requêtes pouvant être émises par le client. Elles peuvent être décomposées en deux éléments :
/
par défaut. Exempleshttp://localhost:8000
définit une requête GET. localhost
est le domaine, :8000
donne le port utilisé. Aucune autre information n’étant spécifiée, le chemin par défaut est /
.http://localhost:8000/xx
transmet une requête GET avec le chemin /xx
.Arrêtez votre application (ctrl-C).
Saisissez la commande suivante : npm install cors morgan nodemon
.
Créez le fichier router.js.
Modifiez le fichier server.js comme suit :
const express = require("express");
const router = require('./router');
const cors = require('cors');
const morgan = require('morgan');
const bodyParser = require("body-parser");
const app = express();
const port = process.env.PORT || 8000;
app.use(morgan('combined'));
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
extended: true
}));
app.use(router); // Requests processing will be defined in the file router
app.listen(port, () => console.log('Server app listening on port ' + port));
Explications :
req.body
.app.use(router)
permet de définir les routes dans le fichier router.js.Modifiez l’entrée scripts de votre fichier package.json comme suit :
"scripts": {
"start": "nodemon server.js",
}
Saisissez la commande npm start
. Ceci déclenche la commande nodemon server.js
. Nodemon est un utilitaire qui relance le serveur à chaque modification.
Avant de relancer votre serveur, il vous faut définir votre router. Recopiez le code suivant dans votre fichier router.js :
const express = require("express");
const router = express.Router();
module.exports = router;
Relancez votre serveur. Remarquez que la requête http://localhost:8000
reçoit un message d’erreur.
Saisissez votre première méthode dans le fichier router.js:
router
.get("/", (req, res) => {
res.json("Hello world!!");
});
Ce code se compose de plusieurs parties qu’il convient de bien comprendre :
.get
est une méthode de l’objet router qui permet de répondre aux requêtes GET. Comme les autres méthode permettant de répondre aux différentes requêtes (POST,, PUT, …), cette méthode comprend deux arguments."/"
est le premier argument de la méthode router.get, il définit le chemin auquelle le routeur réagit.(req,res) => {...}
est le deuxième argument. Il donne la fonction middleware qui sera déclenchée par le routeur lorsque qu’une requête HTTP constituée de la bonne méthode et du bon chemin sera envoyée au serveur. Cette fonction middleware comprend deux arguments :
Remarquez que la requête http://localhost:8000/xx
reçoit encore un message d’erreur. Ajoutez la méthode suivante au routeur afin que celui-ci retourne le statut HTTP approprié au client :
router
.use((req, res) => {
res.status(404);
res.json({
error: "Page not found"
});
});
Le terme use signifie que la fonction middleware est exécutée quelque soit la méthode HTTP utilisée. Ici, le premier argument (le chemin) n’est pas précisé . En conséquence, cette méthode sera exécutée systématiquement lorsqu’elle sera atteinte. Elle doit donc être la dernière de la liste : si aucune méthode du routeur n’a déclenché sa fonction middleware, alors le routeur retourne le message Not found avec un statut d’erreur 404.
Le serveur doit respecter les codes de réponse HTTP, ce qui permet au client de réagir corretcement en fonction du code qu’il reçoit.
Le rôle d’un serveur est d’échanger des données avec un client. Le serveur que nous définissons ici doit donc gérer des données, les transmettre, les modifier, etc. en fonction des échanges avec ses clients.
Pour gérer efficacement les données, un serveur doit utiliser une système de gestion de bases de données (SGBD). Le but de ce chapitre étant d’expliquer le fonctionnement du serveur et de détailler les échanges avec le client, nous allons ici simplifier la partie gestion des données en utilisant un tableau. Evidemment, l’utilisation d’un tableau n’aurait aucun sens dans une application réelle, car à chaque redémarrage du serveur les données seront remises à zéro. Le dialogue entre le serveur et une base de données sera l’objet d’un autre chapitre.
Dans le fichier router.js, créez un tableau avec quelques données basiques :
const persons=[
{id:0,name:'John'},
{id:1,name:'Jane'}
];
Nous utilisons ici un tableau d’objets JavaScript. Remarquez l’emploi du mot clé const
au lieu de let
.
Créez une route qui retourne l’ensemble des personnes. Le chemin est /persons
.
router
.get("/persons",(req,res)=>{
res.json(persons);
})
Dans une application réelle, cette méthode ne serait sans doute pas implémentée car un serveur a pour vocation de contenir un très grand nombre de données. Son rôle est donc de ne transmettre au client que les données dont il a besoin, au moment où le client les lui demande.
Créez une route qui retourne une unique personne
function getPerson(id) {
return persons.find(p => p.id === +id);
}
...
router
.get("/persons/:id",(req,res)=>{
res.json(getPerson(req.params.id));
})
getPerson
utilise la méthode find définie sur les tableaux JavaScript. Dans une application réelle, la fonction getPerson
exécute une requête vers une base de données.:id
dans le chemin de la méthode GET. Cette notation permet de définir des paramètres au chemin reconnu par la méthode GET.Créez une route qui permet d’ajouter une nouvelle personne. Il vous faut pour cela l’aide du package body-parser tel que cela a été expliqué précédemment. Vous pouvez devez ajouter une méthode post pour insérer une personne dans le tableau :
function insertPerson(p) {
p.id = persons.length;
persons.push(p);
return p;
}
...
.post('/person',
(req, res) => {
const p = insertPerson(req.body);
res.status(201)
.set('Location', '/persons/' + p.id)
.json(p);
})
req.body est un objet JavaScript contenant les données transmises au serveur.
L’id de la personne est géré par le serveur, inutile de le lui transmettre.
Les recommandations concernant la gestion d’une requête POST par le serveur se trouvent ici.
Dans notre cas, le serveur retourne un statut 201, conformément aux codes de réponse HTTP.
Pour tester la nouvelle fonctionnalité de votre routeur, vous avez plusieurs possibilités :
Créez une route qui permet de supprimer une personne
function removePerson(id) {
persons = persons.filter(p => p.id !== +id);
}
...
.delete('/person/:id',
(req, res) => {
removePerson(req.params.id);
res
.status(204)
.end();
})
Remarquez l’usage de la méthode filter définie sur les tableaux JavaScript.
Remarquez également le champ req.params
qui permet d’obtenir la valeur de l’id envoyé par le client.
Remarquez enfin que le serveur retourne un statut 204 pour signaler que les données ont bien été supprimées. D’autres codes sont possibles :
Créez une route qui permet de modifier une personne
function updatePerson(person) {
persons = persons.map(p => p.id === +person.id ? person : p);
}
...
.patch('/person/:id',
(req, res) => {
updatePerson(req.body);
res
.status(200)
.json(req.body);
})
Au cours du temps, les fonctionnalités d’un serveur peuvent être ammenées évoluer. Comme le code qui gère les routes a été séparé du code de base de votre serveur, vous pouvez très facilement gérer différentes versions de votre serveur.
Suivant les principes de la gestion sémantique des versions, un numéro de version se compose de trois valeurs séparées par des points :
Pour créer la version 1.0.0 de votre serveur, vous devez redéfinir le chemin qui permet d’accéder au fichier router.js :
const routerV100 = require('./router100');
...
app.use('/1.0.0',routerV100)
Ainsi, le fichier router100.js contient l’ensemble des routes gérées par votre serveur. Pour solliciter le serveur, il faut maintenant ajouter le numéro de version à l’URL. Par exemple, pour extraire l’ensemble des personnes : http://localhost:8000/1.0.0/persons
.
Pour gérer un correctif de cette première version, on crée un fichier router101.js. Le code du serveur devient :
const routerV100 = require('./router100');
const routerV101 = require('./router101');
...
app.use('/1.0.0',routerV100)
app.use('/1.0.1',routerV101)
Ainsi, les utilisateurs de la version 1.0.0 ne sont pas pénalisés, mais ils peuvent passer à la version 1.0.1 à partir de l’URL http://localhost:8000/1.0.1
.
Petits exercices d’application :
persons
contient des données différentes.http://localhost:8000/persons
doit retourner les personnes de la version 1.0.1. Un nouvelle fois, aucune indication ne vous est fournie pour trouver la solution.La source de données actuelle est votre tableau persons
. Cette source de données est distincte de votre serveur, nous verrons plus tard comment remplacer ce tableau par une base de données. Pour le moment, vous allez juste déplacer le code lié aux données dans un répertoire différent de celui qui gère les requêtes des clients :
Créez un répertoire server/db
Créez un fichier server/db/data.js
Déplacez le code directement lié aux données dans data.js :
persons
getPerson
, insertPerson
, removePerson
et updatePerson
dans data.js.Dans data.js, ajoutez la ligne qui suit, qui permet d’exporter les fonctions utiles au routeur :
module.exports = {getPerson, insertPerson, updatePerson, removePerson};
Dans router.js, importez ces fonctions :
const data = require('./db/data');
Ainsi, vous créez un objet JavaScript data
dont les champs sont les fonctions exportées depuis data.js. Dans le routeur, pour extraire la personne 1, par exemple, on écrit donc :
data.getPerson(1)
Remarque : le tableau personnes
n’ayant pas été exporté, il n’est poas accessible directement. Pour récupérer l’ensemble des personnes, il faut écrire une fonction getPersons
.
Le serveur construit précédemment respecte les recommandations d’une API REST. Nous n’étudions pas cette API en détail, néanmoins, le fait de respecter les bases nous permet des créer des interfaces que tout client peut utiliser. Pour plus de détails sur les API REST, vous pouvez vous référer à cette page Les réponses obtenues respectant le format REST, n’importe quelle application peut interroger le serveur et exploiter les resultats obtenus.
Supposons que notre fichier data.js contienne les données suivantes :
let persons = [
{
id: 1, name: 'John',
cities:
[
{id: 1, name: 'Paris', area: 105, population: 2140},
{id: 3, name: 'Warsaw', area: 517, population: 1778000},
{id: 2, name: 'Dublin', area: 115, population: 555000}
]
},
{
id: 3, name: 'Jane',
cities:
[
{id: 2, name: 'Dublin', area: 115, population: 555000}
]
},
{id: 2, name: 'Jack'},
{
id: 3, name: 'Jill',
cities:
[
{id: 2, name: 'Dublin', area: 115, population: 555000}
]
},
{
id: 4, name: 'John',
cities:
[
{id: 3, name: 'Warsaw', area: 517, population: 1778000},
{id: 2, name: 'Dublin', area: 115, population: 555000}
]
},
];
let cities = [
{id: 1, name: 'Paris', area: 105, population: 2140000},
{id: 2, name: 'Dublin', area: 115, population: 555000},
{id: 3, name: 'Warsaw', area: 517, population: 1778000},
{id: 4, name: 'Lisbon', area: 100, population: 506000}
];
Utiliser des noms au pluriel pour obtenir des données
GET /persons
: obtenir la liste des personnes.GET /cities/1
: obtenir la ville dont l’id est égal à 1.GET /cities/1/persons
: obtenir les personnes qui habitent la ville dont l’id est égal à 1. Cette notation reste la même si on remplace GET
par un autre verbe.Ordonner et Filtrer les données
GET /persons?sort=name
: la liste des personnes dans l’ordre lexicographique.GET /persons?name=Jack
: toutes les personnes dont le champ name est Jack.GET /persons?city=1
: la liste des personnes dont l’id de ville est 1.GET /persons?city=1&sort=name
: Les deux critères précédents en même temps.GET /persons?fields=id,name&city=1&sort=name
: La même requête que précédemment, mais on ne retourne que les id et les noms des personnes.GET /persons?fields=id,name/cities?fields=id,name
: Les id et noms des personnes, avec les id et noms de leurs villes.On souhaite créer un nouveau serveur qui intègre les données des deux tableaux ci-dessus et qui répond aux requêtes listées ci-dessous. Ecrivez ce nouveau serveur, et essayer de créer le maximum de services que vous pouvez parmi ceux de la liste :
GET
PUT/POST
PATCH
DELETE
Aide :
Dans une requête HTTP GET, le serveur peut obtenir le complément de la requête dans le champ req.query
.
La méthode split permet de découper une chaine de caractère en un tableau de mots. Par exemple :
const str = 'mot1,mot2,mot3'; // une chaine de mots séparés par des virgules
const tab = str.fields.split(','); // on découpe la chaine en précisant le séparateur
console.log(tab); // ['mot1','mot2','mot3']