Apprendre Express.js : Créer une API REST
Express est un framework JavaScript backend minimaliste se prêtant parfaitement à la création d'API REST.
La création d’API constitue une grande part du développement d’applications en tout genre : web, mobile, IoT, etc.
Dans ce tutoriel, nous allons créer une API Express étape par étape en utilisant l’environnement Node.js.
Configuration de l’environnement
Initialisation d’un projet Express
Commençons par initialiser un projet Node.js avec la ligne de commande suivante :
npm init -y
Il est désormais temps d’installer le framework Express et l’outil de développement Nodemon :
npm install express nodemon
Structuration du projet
Pour cet exemple, nous créerons une API dédiée à la gestion d’artistes musicaux.
L’arborescence de notre API respectera la structure suivante :
📁 demo-api
├── 📁 node_modules
│ ├── 📄 module1
│ ├── 📄 module2
│ └── ...
├── 📁 routes
│ ├── 📄 index.js
│ └── 📄 artist.js
├── 📁 controllers
│ └── 📄 artist.js
├── 📁 services
│ └── 📄 artist.js
├── 📁 datas
│ ├── 📄 artists.json
├── 📁 utils
│ ├── 📄 dataManager.js
├── 📄 app.js
├── 📄 package.lock.json
└── 📄 package.json
📄 app.js
: serveur Express📄 routes/index.js
: fichier regroupant l’ensemble des routes de l’API📄 routes/artist.js
: définition des endpoints (GET
,POST
,PUT
…) de l’API (partie artistes)📄 controllers/artist.js
: construction de la réponse pour chaque endpoint📄 services/artist.js
: implémentation duCRUD
(logique métier)📄 datas/artists.json
: base de donnéesJSON
(à des fins de simplification, notre API REST va interagir avec un fichier.json
et non un réel SGBD)📄 utils/dataManager.js
: Script utilitaire en charge de la manipulation des fichiers de BDD.json
.
Création du serveur
Commençons par créer un serveur on ne peut plus minimaliste, se contentant de répondre au localhost:3000
.
const express = require('express');
const app = express();
const hostname = '127.0.0.1';
const port = process.env.PORT || 3000;
app.listen(port, hostname, () => {
console.log(`Serveur démarré sur http://${hostname}:${port}`);
});
Il est temps de créer un script sur-mesure pour lancer notre serveur avec Nodemon.
{
// ...
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "nodemon app"
},
// ...
}
Si vous effectuez la commande npm run dev
, vous arriverez sur une page blanche indiquant :
Cannot GET /
Et c’est bien normal, car pour le moment, aucune route n’a été définie…
Création du dataset
Créons désormais une base de données orientée document au sein d’un fichier .json
.
Cette BDD contiendra une liste d’artistes, définis par :
- Un identifiant
- Un nom
- Un pays
- Les genres musicaux dans lesquels il s’illustre
Ajouter les données suivantes à l’intérieur :
[
{
"id": 1,
"name": "John Smith",
"country": "United States",
"genres": ["Pop", "Rock"]
},
{
"id": 2,
"name": "Maria Garcia",
"country": "Spain",
"genres": ["Flamenco", "Latin"]
},
{
"id": 3,
"name": "David Kim",
"country": "South Korea",
"genres": ["K-Pop", "Hip-Hop"]
},
{
"id": 4,
"name": "Marta Johnson",
"country": "Canada",
"genres": ["Country", "Folk"]
}
]
Routes
Les routes d’une API REST
, aussi appelées « endpoints », vont opérer sur le serveur et retourner une réponse en fonction des caractéristiques de la requête HTTP.
La définition d’un endpoint se résume à :
- Un chemin
- Une méthode HTTP associée
Lors de la création d’une API, les routes déclenchent généralement les opérations classiques d’un CRUD
(C
reate, R
ead, U
pdate, D
elete).
Nous allons donc implémenter les routes suivantes au sein du fichier de routage 📄 routes/artist.js
.
const express = require("express")
const router = express.Router()
// Tous les artistes
router.get("/", (req, res) => {
// ...
});
// Détails d'un artiste
router.get("/:id", (req, res) => {
// ...
});
// Créer un artiste
router.post("/", (req, res) => {
// ...
});
// Modifier un artiste
router.put("/:id", (req, res) => {
// ...
});
// Supprimer un artiste
router.delete("/:id", (req, res) => {
// ...
});
module.exports = router;
Ce fichier :
- Charge dans
router
le routeur d’Express - Définit 5 endpoints (pour l’instant vides) pour notre
CRUD
- Exporte le routeur
Il serait judicieux de créer un fichier centralisant l’ensemble des routes de l’API, pour simplifier son import ultérieur dans 📄 app.js
.
const express = require("express");
const router = express.Router();
const artistRoutes = require("./artist");
router.use("/artists", artistRoutes);
module.exports = router;
Ce fichier monte les routes associées au CRUD
des artistes sur le routeur en préfixant la route de /artists
.
Focus évolutivité
Dans le cas où notre API manipulerait plusieurs éléments (artistes
, chansons
, albums
…), l’interêt de ce fichier serait d’autant plus marqué :
const express = require("express");
const router = express.Router();
const artistRoutes = require("./artist");
const songRoutes = require("./song");
const albumRoutes = require("./album");
router.use("/artists", artistRoutes);
router.use("/songs", songRoutes);
router.use("/albums", albumRoutes);
module.exports = router;
Il est temps d’importer notre routeur global depuis 📄 app.js
et de le monter sur le chemin /api
:
// ...
const routes = require("./routes");
app.use("/api", routes);
// ...
Désormais, toutes les routes de l’API commenceront par /api
. Les routes définies dans le routeur des artistes, seront quant à elle suivies de /artists
, donnant ainsi :
GET /api/artists
GET /api/artists/:id
POST /api/artists
PUT /api/artists/:id
DELETE /api/artists/:id
Il est temps de créer un contrôleur en charge de construire les réponses HTTP associées aux endpoints des artistes.
Contrôleurs
Les contrôleurs sont en charge de créer la réponse HTTP adaptée à la requête HTTP envoyée par le client. Ce sont eux qui vont définir, au sein de fonctions, les traitements à exécuter pour chaque route de l’API.
const artistService = require('../services/artist');
function list(req, res) {
const artists = artistService.findAll();
res.status(200).json(artists);
}
function read(req, res) {
const artistId = req.params.id;
const artist = artistService.find(artistId);
if (artist)
res.status(200).json(artist);
else
res.status(404).json({ message: "Artiste non trouvé" });
}
function create(req, res) {
const datas = req.body;
const createdArtist = artistService.create(datas);
if (createdArtist)
res.status(201).json({ message: "Artiste créé" });
else
res.status(400).json({ message: "Erreur lors de l'insertion" });
}
function update(req, res) {
const artistId = req.params.id;
const datas = req.body;
const updatedArtist = artistService.update(artistId, datas);
if (updatedArtist) {
res.status(200).json({ message: "Artiste édité" });
} else {
res.status(400).json({ message: "Erreur lors de l'édition" });
}
}
function remove(req, res) {
const artistId = req.params.id;
const removedArtist = artistService.remove(artistId);
if (removedArtist) {
res.status(200).json({ message: "Artiste supprimé" });
} else {
res.status(400).json({ message: "Erreur lors de la suppression" });
}
}
module.exports = {
list,
read,
create,
update,
remove
};
S’agissant d’une API basique, ce contrôleur se contente d’appeler les services (encore inexistants).
Les services vont contenir des fonctions d’interaction (écriture et lecture) avec la base de données. Ce sont eux qui implémentent la logique métier (les opérations de CRUD
) de l’application.
En fonction de la valeur de retour transmise par le service, le contrôleur construira la réponse et lui attachera le code de statut correspondant (200
, 201
, 400
, 404
…).
Parser les données du corps de la requête
create
et update
font appel à req.body
. La propriété body
permet d’extraire le corps de la requête HTTP. C’est de cette manière que l’on peut récupérer les données des artistes envoyées au format JSON :
{
"name": "Patrick Sébastien",
"country": "France",
"genres": ["Beauf", "Variété française"]
}
Pour exploiter les données récupérées par req.body
il faut les parser : transformer le JSON en objet JavaScript
.
Pour cela, il faut exploiter le middleware Express express.json()
. Montons le sur l’ensemble des routes depuis le fichier de route global 📄 routes/index.js
.
const express = require("express");
const router = express.Router();
const artistRoutes = require("./artist");
router.use(express.json());
router.use("/artists", artistRoutes);
module.exports = router;
Mettons désormais à jour notre routeur, en appelant pour chaque endpoint, la fonction appropriée du contrôleur.
const express = require("express");
const router = express.Router();
const artistController = require("../controllers/artist");
// Tous les artistes
router.get("/", artistController.list);
// Détails d'un artiste
router.get("/:id", artistController.read);
// Créer un artiste
router.post("/", artistController.create);
// Modifier un artiste
router.put("/:id", artistController.update);
// Supprimer un artiste
router.delete("/:id", artistController.remove);
module.exports = router;
Services (logique métier)
Vous l’avez compris, nous aurions pu écrire la logique métier de nos opérations CRUD
directement au sein de nos endpoints ou fonctions du contrôleur, mais un peu de découpage ne fait jamais de mal…
Le rôle des services va être d’encapsuler cette logique au sein de fonctions. Mais avant d’écrire la moindre fonction dans notre service, créons un module utilitaire nommé « DataManager », dont le rôle va être d’écrire et lire dans notre BDD JSON.
Gestionnaire de données
Ce module utilitaire, va implémenter les fonctions loadData()
et saveData()
, permettant respectivement de lire et écrire dans des fichiers .json
situés dans le dossier 📂 datas
.
Pour cela, notre gestionnaire de données exploite les méthodes readFileSync()
et writeFileSync()
du module natif fs
.
La fonction validateData()
aura pour sa part le rôle de vérifier que les objets à écrire dans la BDD sont au bon format (possèdent toutes les propriétés requises et nulle autre).
const fs = require("fs");
const dataDirectory = "datas";
function loadData(filename) {
const filePath = `${dataDirectory}/${filename}.json`;
try {
const data = fs.readFileSync(filePath, "utf8");
return JSON.parse(data);
} catch (err) {
console.error(`Impossible de lire le fichier ${filename}.json :`, err);
}
}
function saveData(filename, data) {
const filePath = `${dataDirectory}/${filename}.json`;
try {
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
} catch (err) {
console.error(
`Impossible d'écrire dans le fichier ${filename}.json :`,
err
);
}
}
function validateData(data, requiredKeys) {
const keys = Object.keys(data);
if (keys.length !== requiredKeys.length) {
return false;
}
for (const key of requiredKeys) {
if (!data.hasOwnProperty(key)) {
return false;
}
}
return true;
}
module.exports = { loadData, saveData, validateData };
CRUD
Il est enfin temps d’implémenter le CRUD
des artistes.
Mettons en place le squelette de notre service en :
- Important les fonctions utiles du gestionnaire de données (
📄 dataManager.js
) - Chargeant les données de la BDD dans la variable
artists
- Définissant les 5 fonctions du
CRUD
:findAll()
,find()
,create()
,update()
etremove()
. - Exportant ces fonctions
const { loadData, saveData, validateData } = require("../utils/dataManager");
const filename = "artists";
let artists = loadData(filename);
function findAll() { ... }
function find(id) { ... }
function create(newArtist) { ... }
function update(id, updatedArtist) { ... }
function remove(id) { ... }
module.exports = {
findAll,
find,
create,
update,
remove,
};
Il est temps d’implémenter la logique métier au sein des 5 fonctions.
1. Liste des artistes
function findAll() {
return artists;
}
On se contente de retourner les artistes stockés dans 📄 datas/artists.json
.
2. Détails d’un artiste
function find(id) {
return artists.find((artist) => artist.id === parseInt(id));
}
On retourne l’artiste ayant l’id transmis à la fonction dans 📄 datas/artists.json
.
3. Création d’un artiste
function create(newArtist) {
if (!validateData(newArtist, ["name", "country", "genres"])) return false;
const id = artists.length > 0 ? artists[artists.length - 1].id + 1 : 1;
const artist = { id, ...newArtist };
artists.push(artist);
saveData(filename, artists);
return artist;
}
On ajoute à la fin du tableau artists
un nouvel artiste en lui définissant un id automatiquement incrémenté en se basant sur l’id de l’artiste précédent + 1.
Le tableau artists
étant mis à jour, on demande à ce qu’il remplace le contenu du fichier 📄 datas/artists.json
.
4. Modification d’un artiste
function update(id, updatedArtist) {
if (!validateData(updatedArtist, ["name", "country", "genres"])) return false;
let index = artists.findIndex((artist) => artist.id === parseInt(id));
if (index === -1) return false;
artists[index] = { id: parseInt(id), ...updatedArtist };
saveData(filename, artists);
return artists[index];
}
On modifie dans le tableau artists
, les propriétés de l’artiste ayant l’id transmis à la fonction.
Le tableau artists
étant mis à jour, on demande à ce qu’il remplace le contenu du fichier 📄 datas/artists.json
.
5. Suppression d’un artiste
function remove(id) {
const index = artists.findIndex(artist => artist.id === parseInt(id));
if (index === -1) return false;
artists.splice(index, 1);
saveData(filename, artists);
return true;
}
On supprime dans le tableau artists
, l’artiste étant situé à la position de l’objet ayant l’id transmis à la fonction.
Le tableau artists
étant mis à jour, on demande à ce qu’il remplace le contenu du fichier 📄 datas/artists.json
.
Tester l’API
S’il est facile de tester des requêtes GET
et POST
au sein de son navigateur, les méthodes PUT
, PATCH
et DELETE
ne sont pas directement prises en charge par les navigateurs web.
C’est là que des outils de test API tels que Postman ou Insomnia s’avèrent indispensables.
Ils offrent une interface conviviale permettant de construire, envoyer et visualiser des requêtes HTTP pour toutes les méthodes HTTP, y compris PUT
, PATCH
et DELETE
.
Avec ces outils, il devient aisé pour les développeurs de simuler et inspecter les réponses des serveurs, ce qui facilite le débogage et le développement d’API REST.