b) Introduction au JSON et persistance des données
Le JSON, c'est quoi ?
Précédemment, nous avons développé notre première RESTful API.
Nous avons implicitement découvert le JSON, notamment lorsque nous avons fait des requêtes vers nos API.
Nous allons maintenant voir ce que permet le JSON, notamment la création de fichiers permettant de sauvegarder des données facilement en JS.
JSON vient de JavaScript Object Notation.
C'est une syntaxe pour échanger et faire persister des données.
Du JSON, c'est du texte en notation JS.
Voici les types de données qui sont valides en JSON :
string
number
object
array
boolean
null
⚡ Il n'y a donc pas de function
, date
et undefined
.
Voici un exemple de représentation de données en JSON qui correspond à ce que très souvent une API renvoie, un array d'objets :
json[{"email": "raphael@voila.com","fullname": "Raphael Baroni"},{"email": "jkj@herenqn.com","fullname": "JK Roling"},{"email": "serena@gmail.com","fullname": "Serena Here"}]
Communication de données en JSON à une API
Introduction
Dans le tutoriel précédent, nous avons communiqué des données au format JSON :
- soit lors des requêtes via REST Client : nous avons envoyé les données permettant de créer ou modifier une pizza :
- soit lors du traitment des requêtes par l'API, afin de créer ou modifier les pizzas et les sauvegarder en mémoire vive (dans un tableau d'objets).
Nous allons maintenant approfondir comment les données au format JSON ont été traitées par l'API.
Envoi de données d'une API vers un client & sérialisation
Via Express, nous pouvons très facilement convertir un objet JS en JSON afin de l'envoyer vers une application cliente grâce à la méthode res.json()
.
C'est ce que nous appelons de la sérialisation de données : nous passons du monde "objets en mémoire" vers du texte (ou des octets) qui va voyager sur un réseau.
Le code actuel de notre RESTful API, renvoyant un array de pizzas au format JSON, est géré automatiquement via :
jsres.json(orderedMenu ?? MENU);
Lorsque l'API renvoie MENU
avec les pizzas par défaut, voici le JSON qui voyage sur le réseau :
json[{"id": 1,"title": "4 fromages","content": "Gruyère, Sérac, Appenzel, Gorgonzola, Tomates"},{"id": 2,"title": "Vegan","content": "Tomates, Courgettes, Oignons, Aubergines, Poivrons"},{"id": 3,"title": "Vegetarian","content": "Mozarella, Tomates, Oignons, Poivrons, Champignons, Olives"},{"id": 4,"title": "Alpage","content": "Gruyère, Mozarella, Lardons, Tomates"},{"id": 5,"title": "Diable","content": "Tomates, Mozarella, Chorizo piquant, Jalapenos"}]
💭 La puissance du JSON peut déjà s'exprimer ici. Mais comment ?
L'API renvoie un array d'objets, des pizzas, au format JSON, qui correspond en fait à un format texte avec des conventions.
Il est donc possible à n'importe quelle application cliente d'utiliser ces données, quelque soit la technologie, le langage utilisé pour développer cette application cliente.
Ainsi, par exemple, une application Android, développée en Java, pourrait consommer cette API pour afficher un menu de pizzas !
Réception de données d'un client par une API & parsing
Via Express, nous pouvons très facilement convertir du JSON vers un objet JS à l'aide du middleware express.json()
.
C'est ce que nous appelons du parsing de données, ou de la désérialisation : nous passons du monde texte / JSON (ou des octets) vers des "objets en mémoire".
Le code actuel de notre RESTful API, récupérant les données d'une pizza à créer, est très simple :
jsconst title = req?.body?.title?.length !== 0 ? req.body.title : undefined;const content = req?.body?.content?.length !== 0 ? req.body.content : undefined;
Automatiquement, grâce à Express et au middleware appelé dans apps.js
(app.use(express.json());
), req.body
contient un objet JS représentant toutes les données JSON qui étaient présentes dans le body de la requête cliente, comme par exemple :
http### Create a pizzaPOST {{baseUrl}}/pizzasContent-Type: application/json{"title":"Magic Green","content":"Epinards, Brocolis, Olives vertes, Basilic"}
Il est important de communiquer le "media type" (ou MIME type) dans le corps de la requête : application/json
.
Cela indique à l'application qui est la cible de la requête (l'API dans notre cas) quelle genre d'outil elle devra utiliser pour décoder les données.
Par exemple, il faut faire un traitement différent pour récupérer des données au format JSON que pour récupérer des données directement associées à un fichier image.
Lecture et sauvegarde de données dans un fichier JSON par une API
Lecture de donn ées se trouvant dans un fichier JSON
La fonction JSON.parse(objectSerialized)
permet de créer un objet JS à partir d'une string
contenant des données au format JSON.
Par exemple, voici une fonction, permettant à une application Express de créer un objet JS en lisant des données se trouvant dans un fichier .json
dont le chemin et nom complet sont indiqués dans le paramètre filePath
:
js/*** Parse items given in a .json file* @param {String} filePath - path to the .json file* If the file does not exist or it's content cannot be parsed as JSON data,* use the default data.* @param {Array} defaultArray - Content to be used when the .json file does not exists* @returns {Array} : the array that was parsed from the file (or defaultData)*/function parse(filePath, defaultArray = []) {if (!fs.existsSync(filePath)) return defaultArray;const fileData = fs.readFileSync(filePath);try {// parse() Throws a SyntaxError exception if the string to parse is not valid JSON.return JSON.parse(fileData);} catch (err) {return defaultArray;}}
Imaginez que plutôt que de lire le menu de pizza à partir d'un array d'objets, on souhaite lire ce menu grâce au contenu d'un fichier contenant du JSON. Voici ce que donnerait l'opération de lecture de toutes les pizzas si le chemin et nom complet du fichier JSON était donné dans la constante jsonDbPath. :
jsconst jsonDbPath = __dirname + '/../data/pizzas.json';// Read all the pizzas from the menurouter.get('/', function (req, res) {console.log('GET /pizzas');const pizzas = parse(jsonDbPath, DEFAULT_MENU);res.json(pizzas);});
Sauvegarde de données dans un fichier JSON
La fonction JSON.stringify(objectToSerialised)
permet de créer une string
contenant la représentation JSON de l'objet à sérialiser.
Côté serveur, il est ensuite facile de sauvegarder les données JSON au sein d'un fichier.
Par exemple, voici une fonction permettant à une application Express de sauvegarder au format JSON un objet dans un fichier .json
dont son chemin et nom complet sont indiqués dans le paramètre filePath
:
js/*** Serialize the content of an Object within a file* @param {String} filePath - path to the .json file* @param {Array} object - Object to be written within the .json file.* Even if the file exists, its whole content is reset by the given object.*/function serialize(filePath, object) {const objectSerialized = JSON.stringify(object);fs.writeFileSync(filePath, objectSerialized);}
Imaginez que, au sein du router traitant des ressources de type "pizzas", vous passiez la valeur suivante à filePath
:
jsconst jsonDbPath = __dirname + '/../data/pizzas.json';serialize(jsonDbPath, MENU);
Cela signifie que dans le projet contenant notre API, nous allons sauvegarder le menu des pizzas au format JSON dans le fichier JSON /data/pizzas.json
.
Ce fichier est en fait une base de données simplifiée !
Persistance des données : d'une sauvegarde en mémoire vive vers un fichier JSON
Nous allons maintenant réaliser un tutoriel pour rendre les ressources de type "pizzas" persistantes.
Nous allons repartir de l'API créée au tutoriel précédent.
Dans votre repo web2
, veuillez copier / coller le répertoire /tutorials/pizzeria/api/basic
et le renommer en /tutorials/pizzeria/api/persistence
.
En cas de souci, vous pouvez télécharger le code du tutoriel précédent ici : api-basic.
Veuillez ouvrir un terminal au niveau de ce répertoire.
Pour la suite du tutoriel, nous considérons que tous les chemins absolus démarrent du répertoire /tutorials/pizzeria/api/persistence
(ou /web2/tutorials/pizzeria/api/persistence
si l'on considère le nom du répertoire du repo).
Veuillez créer un nouveau répertoire /utils
. Au sein de ce répertoire, veuillez créer le module /utils/json.js
dans lequel vous allez ajouter ces fonctions :
jsconst fs = require('fs');/*** Parse items given in a .json file* @param {String} filePath - path to the .json file* If the file does not exist or it's content cannot be parsed as JSON data,* use the default data.* @param {Array} defaultArray - Content to be used when the .json file does not exists* @returns {Array} : the array that was parsed from the file (or defaultArray)*/function parse(filePath, defaultArray = []) {if (!fs.existsSync(filePath)) return defaultArray;const fileData = fs.readFileSync(filePath);try {// parse() Throws a SyntaxError exception if the string to parse is not valid JSON.return JSON.parse(fileData);} catch (err) {return defaultArray;}}/*** Serialize the content of an Object within a file* @param {String} filePath - path to the .json file* @param {Array} object - Object to be written within the .json file.* Even if the file exists, its whole content is reset by the given object.*/function serialize(filePath, object) {const objectSerialized = JSON.stringify(object);createPotentialLastDirectory(filePath);fs.writeFileSync(filePath, objectSerialized);}/**** @param {String} filePath - path to the .json file*/function createPotentialLastDirectory(filePath) {const pathToLastDirectory = filePath.substring(0, filePath.lastIndexOf('/'));if (fs.existsSync(pathToLastDirectory)) return;fs.mkdirSync(pathToLastDirectory);}module.exports = { parse, serialize };
L'opération de sérialisation des données est faite via la fonction serialize
de /utils/json.js
. Pour se simplifier la vie et ne pas obliger les développeurs à devoir créer manuellement un répertoire qui contiendra la mini DB de pizzas (le fichier pizzas.json
dans la suite de l'exemple), une fonction a été créée qui s'appelle createPotentialLastDirectory
.
La fonction serialize
fait appel à cette fonction qui va, si nécessaire, créer le dernier répertoire donné dans le chemin vers le fichier JSON (le répertoire /data
dans la suite de l'exemple).
Il n'est pas intéressant de retenir par coeur le code donné dans /utils/json.js
. Par contre, il est important que vous compreniez celui-ci, ce qu'il fait.
A présent, nous allons convertir le code du router de "pizzas" pour rendre les données peristantes.
Voici ce que nous devons faire pour les opérations de :
- lecture de ressources : il suffit de faire appel à la fonction
parse
qui tentera de charger les ressources qui devraient se trouver dans le répertoire/data/pizzas.json
. Notons que le chemin vers ce fichier JSON est un simple choix, il doit être configurable. - écriture de ressources : lors d'une opération d'écriture pour créer une nouvelle ressource, ou pour mettre à jour une ressource existante, voici les étapes :
- création d'une liste de toutes les pizzas dans un array : cela correspond à l'utilisation de la fonction
parse
pour tenter de charger ce qui est contenu dans la mini DB de pizzas. - mise à jour de l'array soit en ajoutant un nouvel objet (une pizza), soit en modifiant un objet existant, soit en supprimant un objet.
- réécriture complète du fichier JSON contenant la liste de pizzas sur base de l'array de pizzas qui a précédemment été mis à jour via la méthode
serialize
.
- création d'une liste de toutes les pizzas dans un array : cela correspond à l'utilisation de la fonction
Voici le code du router mis à jour afin de gérer la persistance selon la strat égie définie ci-dessus, les modifications étant surlignées :
js1var express = require('express');2const { serialize, parse } = require('../utils/json');3var router = express.Router();45const jsonDbPath = __dirname + '/../data/pizzas.json';67const MENU = [8{9id: 1,10title: '4 fromages',11content: 'Gruyère, Sérac, Appenzel, Gorgonzola, Tomates',12},13{14id: 2,15title: 'Vegan',16content: 'Tomates, Courgettes, Oignons, Aubergines, Poivrons',17},18{19id: 3,20title: 'Vegetarian',21content: 'Mozarella, Tomates, Oignons, Poivrons, Champignons, Olives',22},23{24id: 4,25title: 'Alpage',26content: 'Gruyère, Mozarella, Lardons, Tomates',27},28{29id: 5,30title: 'Diable',31content: 'Tomates, Mozarella, Chorizo piquant, Jalapenos',32},33];3435/* Read all the pizzas from the menu36GET /pizzas?order=title : ascending order by title37GET /pizzas?order=-title : descending order by title38*/39router.get('/', (req, res, next) => {40const orderByTitle =41req?.query?.order?.includes('title') ? req.query.order : undefined;42let orderedMenu;43console.log(`order by ${orderByTitle ?? 'not requested'}`);4445const pizzas = parse(jsonDbPath, MENU);4647if (orderByTitle) orderedMenu = [...pizzas].sort((a, b) => a.title.localeCompare(b.title));48if (orderByTitle === '-title') orderedMenu = orderedMenu.reverse();4950console.log('GET /pizzas');51return res.json(orderedMenu ?? pizzas);52});5354// Read the pizza identified by an id in the menu55router.get('/:id', (req, res) => {56console.log(`GET /pizzas/${req.params.id}`);5758const pizzas = parse(jsonDbPath, MENU);5960const indexOfPizzaFound = pizzas.findIndex(pizza => pizza.id == req.params.id);6162if (indexOfPizzaFound < 0) return res.sendStatus(404);6364return res.json(pizzas[indexOfPizzaFound]);65});6667// Create a pizza to be added to the menu.68router.post('/', (req, res) => {69const title = req?.body?.title?.length !== 0 ? req.body.title : undefined;70const content = req?.body?.content?.length !== 0 ? req.body.content : undefined;7172console.log('POST /pizzas');7374if (!title || !content) return res.sendStatus(400); // error code '400 Bad request'7576const pizzas = parse(jsonDbPath, MENU);77const lastItemIndex = pizzas?.length !== 0 ? pizzas.length - 1 : undefined;78const lastId = lastItemIndex !== undefined ? pizzas[lastItemIndex]?.id : 0;79const nextId = lastId + 1;8081const newPizza = {82id: nextId,83title: title,84content: content,85};8687pizzas.push(newPizza);8889serialize(jsonDbPath, pizzas);9091return res.json(newPizza);92});9394// Delete a pizza from the menu based on its id95router.delete('/:id', (req, res) => {96console.log(`DELETE /pizzas/${req.params.id}`);9798const pizzas = parse(jsonDbPath, MENU);99100const foundIndex = pizzas.findIndex(pizza => pizza.id == req.params.id);101102if (foundIndex < 0) return res.sendStatus(404);103104const itemsRemovedFromMenu = pizzas.splice(foundIndex, 1);105const itemRemoved = itemsRemovedFromMenu[0];106107serialize(jsonDbPath, pizzas);108109return res.json(itemRemoved);110});111112// Update a pizza based on its id and new values for its parameters113router.patch('/:id', (req, res) => {114console.log(`PATCH /pizzas/${req.params.id}`);115116const title = req?.body?.title;117const content = req?.body?.content;118119console.log('POST /pizzas');120121if ((!title && !content) || title?.length === 0 || content?.length === 0) return res.sendStatus(400);122123const pizzas = parse(jsonDbPath, MENU);124125const foundIndex = pizzas.findIndex(pizza => pizza.id == req.params.id);126127if (foundIndex < 0) return res.sendStatus(404);128129const updatedPizza = {...pizzas[foundIndex], ...req.body};130131pizzas[foundIndex] = updatedPizza;132133serialize(jsonDbPath, pizzas);134135return res.json(updatedPizza);136});137138module.exports = router;
Veuillez mettre à jour votre fichier /router/pizzas.js
sur base du code donné et testez le bon fonctionnement de l'application. Faites quelques requêtes pour ajouter et modifier des données et vérifiez, une fois que vous redémarrer votre application, que les données persistent.
Exercice 1.7 : Persistance des données
Vous allez mettre à jour la RESTful API de myMovies afin de rendre les données persistantes dans un fichier JSON : /data/films.json
.
Veuillez repartir du code de la solution de votre Exercice 1.6.
Le code de votre application doit se trouver dans votre repository local et votre web repository (normalement appelé web2
) dans le répertoire nommé /exercises/1.7
.
Veuillez tester toutes les fonctions de la RESTful API pour la collection de films à l'aide de REST Client en copiant les requêtes développées pour l'exercice précédent (fichier films.http du répertoire REST Client). Normalement, il n'y a pas de nouvelles requêtes à écrire, il suffit juste de les exécuter.
Veuillez faire un commit
de votre code avec le message suivant : 1.7 : API : persistence
.