API Node.js, Express et MongoDB

Image de la série de tutoriels pour la création d'une API Node.js, Express, et MongoDB
Image de l'article concernant la création d'une API avec Node.js, Express et MongoDB

Série Tutoriels : Création d'une API avec Node.js, Express et MongoDB

Comme toujours sur Activateur Web, cette série est réalisée sous la forme de vidéos tutoriels et également sous la forme de tutoriels à lire pour vous guider pas à pas. Pour chacun des chapitres ci dessous, vous aurez donc une vidéo, et en dessous de celle ci , le tutoriel à lire qui vous guidera pas à pas, avec le code que vous pouvez copier au fur et à mesure. 

Pour ceux qui suivront uniquement les vidéos, sachez que le code complet de chaque fichier sera mis à disposition à la fin de cette série consacrée à la création d’une api avec Node.js, Express, et MongoDB. 

Codes couleurs pour plus de compréhension

Pour la compréhension de cette série, pour ceux qui suivront le format à lire, j’ai indiqué le nom d’un dossier est de couleur verte, le nom d’un fichier est de couleur orange, celui d’une fonction est en mauve, et celui d’un package est en bleu.

Table des matières - Sommaire

Introduction

Dans cette nouvelle série de tutoriels, nous verrons comment construire une API, avec le runtime Node.js, le framework Express, et le gestionnaire de bases de données MongoDB.
Comme toujours, rendons à César ce qui appartient à César ! Le projet que nous allons réaliser tout au long de cette série est inspiré d’un projet que j’ai dû réaliser lors de ma formation de développeur web chez OpenClassRooms. Et j’ai créé cette série, en relisant l’excellent cours de William Alexander, sur le développement d’un projet full stack en codant la partie backend d’un projet. L’application frontend que nous utiliserons lors de cette série tutoriel est une adaptation de l’application frontend Hot Takes du même William Alexander.
Pour créer notre API, j’utiliserai donc Node.js, Express et MongoDB, mais avant de se lancer dans le code, je veux juste rappeler ici, ce qu’est une API, ce qu’est Node.js, ce qu’est Express, et ce qu’est MongoDB. 

API : interface de programmation applicative

Application  Programming  Interface

Une API permet d’exposer sur le web ou sur un réseau local d’entreprise un catalogue de fonctionnalités issues d’une application, ou encore des contenus en provenance d’une base de données ou d’un système de fichiers. 

Objectif : permettre à des systèmes tiers d’accéder à ces fonctionnalités et ces contenus.

Node.js : plateforme de développement

Node.js est une plateforme qui permet aux développeurs de créer des applications côté serveur avec JavaScript. 

Son utilisation des E/S (entrées/sorties) non bloquantes, permet de construire des applications modulaires et asynchrone.

Il permet de créer des applications rapides et efficaces, et facilement évolutives au fur et à mesure de leur développement. 

Node.js est également fourni avec des outils permettant de gérer les dépendances, d’exécuter des tests unitaires et de compiler les données.

Son gestionnaire de paquets NPM, est très facile à utiliser, et nous disposons ainsi de plus d’un million de paquets en open source que nous pouvons utiliser pour développer nos applications.

Express.js : Framework backend Node.js

Express.js, plus souvent nommé Express, est un framework backend Node.js, minimaliste, et rapide qui offre des fonctionnalités et des outils robustes pour développer des applications backend évolutives. 

Express permet de bénéficier de fonctionnalités avancées et simplifiées pour un serveur Node.js, ainsi qu’un système de routage très facile à mettre en œuvre.

Avec Express, nous mettrons en place des middlewares, qui sont des codes nous permettant d’interférer avant ou après une requête client.

Le système de routage Express est un système robuste et puissant intégré dès le départ. Il nous permettra de gérer la structure de notre application facilement. 

MongoDB : Base de données NoSQL

MongoDB est une base de données NoSQL orientée document. Elle se distingue des bases de données relationnelles par sa flexibilité et ses performances.

Contrairement à une base de données relationnelle SQL traditionnelle, MongoDB ne repose pas sur des tableaux et des colonnes. Les données sont stockées sous forme de collections et de documents.

Les documents sont des paires de valeurs / clés servant d’unité de données de base. Les collections quant à elles contiennent des ensembles de documents et de fonctions. Elles sont l’équivalent des tableaux dans les bases de données relationnelles classiques.

Mongoose : Package Node.js

Mongoose est un package Node.js qui servira de passerelle entre Node.js et la base de données MongoDB. 

Mongoose dispose de plusieurs fonctionnalités nous permettant d’implémenter notre CRUD.

Pitch avec spécifications techniques et fonctionnelles du projet

Pitch du projet

Un prospect vous a contacté, car il aimerait savoir si vous êtes capable de lui fournir une application web recueillant les avis de consommateurs de bières.

 Cette application s’appellera Beer Paradise

Si à terme cette application deviendra, peut être, une boutique en ligne, cette première version sera une application d’évaluation. 

Cette version sera une exposition de bières permettant aux utilisateurs d’ajouter leurs bières préférées et de liker ou disliker les bières que d’autres partagent. 

Le front-end de l’application a été développé à l’aide d’Angular et a été précompilé après des tests internes, mais Beer Paradise a besoin d’un développeur back-end pour construire l’API. 

Vous décidez de répondre favorablement à cette demande. Suite à cela, Beer Paradise vous envoie les spécifications techniques et fonctionnelles suivantes :

Contexte du projet

Afin de connaître les bières les plus plébiscitées par les consommateurs, l’entreprise souhaite, dans un premier temps, créer une application web où les utilisateurs pourront ajouter leurs bières et également liker ou disliker les bières ajoutées par les autres. 

Spécifications de l’API

API Errors

Les erreurs éventuelles doivent être renvoyées telles qu’elles sont produites, sans modification ni ajout. Si nécessaire, utilisez une nouvelle Error().

API Routes

Toutes les routes beer pour les bières doivent disposer d’une autorisation (le token est envoyé par le frontend avec l’en-tête d’autorisation : « Bearer <token> »). Avant que l’utilisateur puisse apporter des modifications à la route bière, le code doit vérifier si l’userId actuel correspond à l’userId de la bière. Si l’userId ne correspond pas, renvoyer un code d’erreur adéquat. Cela permet de s’assurer que seul le propriétaire (manufacturer) de la bière peut apporter des modifications à celle-ci.

Modèles de données

Les modèles de données doivent être : 

Pour les bières

  • userId : String — l’identifiant MongoDB unique de l’utilisateur qui a créé la bière.
  • name : String— nom de la bière.
  • manufacturer :String — celui qui a posté la bière.
  • description : String — description de la bière.
  • mainIngredient :String— le principal ingrédient de la bière.
  • imageUrl:String — l’URL de l’image de la bière téléchargée par l’utilisateur.
  • degree :Number— nombre entre 1 et 10 décrivant le degré d’alcool de la bière.
  • likes: Number— nombre d’utilisateurs qui aiment (= likent) la bière. 
  • dislikes : Number— nombre d’utilisateurs qui n’aiment pas (= dislike) la bière.
  • usersLiked: [ “String <userId>” ] — tableau des identifiants des utilisateurs qui ont aimé (= liked) la bière.
  • usersDisliked : [ “String <userId>” ] — tableau des identifiants des utilisateurs qui n’ont pas aimé (= disliked) la bière.

 

Pour les utilisateurs

  • email : String — adresse e-mail de l’utilisateur [doit être unique]. 
  • password : String— mot de passe de l’utilisateur haché.

Exigences de sécurité

Le mot de passe de l’utilisateur doit être haché. 

  • L’authentification doit être renforcée sur toutes les routes “bières” requises. 
  • Les adresses électroniques dans la base de données sont uniques et un plugin Mongoose approprié est utilisé pour garantir leur unicité et signaler les erreurs. 
  • La sécurité de la base de données MongoDB (à partir d’un service tel que MongoDB Atlas) ne doit pas empêcher l’application de se lancer sur la machine d’un utilisateur. 
  • Un plugin Mongoose doit assurer la remontée des erreurs issues de la base de données. 
  • Les versions les plus récentes des logiciels sont utilisées avec des correctifs de sécurité actualisés. 
  • Le contenu du dossier images ne doit pas être téléchargé sur GitHub.

Repository GitHub

Vous pouvez si vous le souhaitez télécharger ou cloner le projet front-end de l’application Beer Paradise.

Tout au long de cette série tutoriel, nous utiliserons à la fois l’application frontend et l’outil Postman pour tester nos fonctionnalités en développement. 

Je vous donne la marche à suivre en début de tutoriel.

Prérequis pour suivre cette série de tutoriels

Pour suivre ce tutoriel, vous devez avoir les bases en Javascript, et avoir pratiqué ce langage au moins sur la partie frontend d’une application. Malgré tout, je vous explique le code que nous écrirons au fur et à mesure du développement de l’API.

Outils nécessaires :

Éditeur de code

  • Un éditeur de code : Pour ma part, j’utilise VS Code et c’est cet éditeur que j’utiliserai tout au long de cette série de tutoriels. Vous pouvez bien entendu, en utiliser un autre.

Si vous n’avez pas encore d’éditeur de code, n’hésitez pas à suivre l’article de blog que j’ai réalisé sur l’installation de Visual Studio Code.

Plateforme de développement

  • Node.js et son gestionnaire de paquets Npm. Bien évidemment, pour construire une API avec Node.js, il faut que Node.js et Npm soient installés sur votre système.

Si vous n’avez pas encore installé Node.js et Npm, j’ai là encore écrit un article de blog sur le sujet. N’hésitez pas à le suivre.

Frontend de l'application

  • Angular : La partie front-end de l’application a été créée et compilée avec Angular. Il faudra donc installer la CLI Angular sur votre système. Mais ne vous inquiétez pas, je vous explique son installation avec Npm en tout début de tutoriel. A noter que j’aurai pu mettre Angular dans les outils facultatifs. En effet, rien ne vous empêche de créer L’API, de la tester avec l’outil Postman, par exemple, et de créer votre propre frontend. 

Base de données

Dans cette série, je ne détaillerai pas la création d’un compte MongoDB, puisque je l’ai déjà fait dans le tutoriel cité précédemment. Nous verrons par contre comment connecter la base de données MongoDB à Node.js avec le module Mongoose. 

Ligne de commande depuis le terminal

  • Terminal et ligne de commande : Tout au long de cette série, nous utiliserons le terminal et sa ligne de commande afin de lancer nos serveurs, d’exécuter des commandes d’installation de packages, etc. Même si à chaque fois, je vous détaillerai la ligne de commande à saisir, il est bien de savoir utiliser un terminal. 

J’ai donc là aussi, si vous souhaitez comprendre l’utilisation d’un terminal, un tutoriel dédié sur le blog, qui vous aidera. N’hésitez pas à revoir cela au besoin. 

Outils facultatifs

Les outils suivant sont facultatifs pour pouvoir réaliser le projet de la série :

  • Git : Si vous souhaitez versionner le projet tout au long de son développement, un compte GitHub avec un repository dédié, sera nécessaire. Il vous faudra également avoir installé Git sur votre système. 
  • Postman : L’outil Postman nous permettra de tester notre application et les routes de l’API, tout au long, du développement du projet.Nous pourrons tester avec notre frontend, mais également avec Postman. Il n’est pas obligatoire, mais sachez qu’une API dispose en principe de sa documentation, et dans ce cas, Postman nous sera d’une grande aide. La documentation de l’API, ne sera pas réalisée lors de cette série mais donnera lieu à un tutoriel spécifique, par la suite. Nous avons déjà beaucoup de choses à faire pour créer notre API. 

Conclusion de l’introduction

Voici pour l’introduction de cette série de tutoriels consacrée à la création d’une API avec Node.js, Express et MongoDB.

Nous pouvons maintenant commencer à ouvrir notre éditeur de code, et se lancer dans la réalisation de ce projet. 

Alors si vous prêts à apprendre, 

  • à construire une API REST avec Node.js, Express et MongoDB ,
  • à créer un serveur web simple avec Express,
  • à mettre en place un système d’authentification sécurisé,
  • à gérer des fichiers utilisateurs,
  • à implémenter un CRUD complet ? 

Et bien suivez moi… 

Initialisation du projet

Installation de la CLI Angular

Je vous l’ai dit en introduction, pour utiliser le dossier frontend créé pour ce projet, nous aurons besoin de la CLI Angular sur notre système. 

Si Angular n’est pas installé sur votre système, vous ouvrez un terminal, et dans la ligne de commande, vous tapez :

				
					npm install @angular/CLI -g
				
			

Votre pare-feu peut vous demander une autorisation pour ouvrir les ports, ce qui est normal. 

Ci-dessus, je rappelle juste que le flag “-g” indique que nous installons la CLI Angular de manière globale sur notre système. Ainsi, nous y aurons accès sur tous les projets le nécessitant. Nous pouvons fermer le terminal, pour le moment, en tapant :

				
					exit
				
			

Création du dossier du projet

Nous commencerons par créer un dossier de projet, que nous pouvons peut-être nommer API_FullStack, par exemple. Mais libre à vous de nommer vos dossiers comme vous le souhaitez et là où vous le voulez. L’important étant de se rappeler où ils se trouvent et de bien faire la liaison ensuite.

J’ouvre ensuite ce dossier API_FullStack dans mon éditeur de code.

Clonage de la partie frontend du projet.

La partie frontend du projet a été réalisée, et elle nous servira d’application web, bien sûr, mais de surtout tester si les fonctionnalités que nous allons coder dans notre API, donnent le résultat escompté. Cette partie frontend du projet ne doit pas être modifiée sous peine de ne plus fonctionner. La seule chose à faire avec cette partie est donc de la cloner, de l’installer et de s’en servir.

Je vais toutefois, vous expliquer 2 méthodes possibles : 

1ère méthode : Pour ceux qui ont Git installé sur leur système :

Pour cloner la partie frontend du projet, ouvrez un terminal depuis le dossier API_FullStack, et taper ensuite en ligne de commande :

				
					git clone https://github.com/FabRiviere/serieAW-Construire_API-Front-end.git frontend
				
			

Après avoir taper sur la touche Entrée, un dossier frontend comprenant tout le code de l’application frontend doit être présent dans votre dossier API_FullStack.

Depuis la ligne de commande, entrez dans votre nouveau dossier frontend en tapant :

				
					cd frontend

				
			

Votre ligne de commande doit maintenant se finir par …./API_FullStack/frontend/

Installer maintenant l’application frontend en tapant : 

				
					npm install
				
			

L’installation va prendre quelques temps, en fonction de votre système, de votre connexion, etc. 

Une fois l’installation terminée, lancer le serveur frontend de l’application en tapant : 

				
					ng serve
				
			

Le serveur frontend est lancé. Ouvrez votre navigateur web, et saisissez dans la barre d’adresse : 

http://localhost:4200

L’application frontend doit s’afficher correctement. 

Image de l'application frontend Beer Paradise utilisable après la création de l'API
Application frontend Beer Paradise utilisable après la création de l'API

2ème méthode : Pour ceux qui n’ont pas Git sur leur système

Sachez tout d’abord qu’un développeur se doit de “versionner” son code, et donc de disposer d’un service tel que Git et GitHub. Imaginez que votre projet est presque fini et que votre ordinateur vous lâche, et bien si vous ne l’avez pas versionné et hébergé quelque part, vous devrez tout recommencer.. 

Ceci étant dit, pour ceux qui n’ont pas encore Git sur leur système, vous pouvez tout de même utiliser l’application frontend. 

Pour cela, sur votre navigateur web, commencer par vous rendre à cette adresse :

https://github.com/FabRiviere/serieAW-Construire_API-Front-end

Ensuite, vous allez cliquer sur le bouton vert indiquant “<> Code”, en haut à droite. 

Dans la liste de ce bouton, vous avez un sous-menu appelé “Download ZIP”. Cliquez sur ce choix. Une fenêtre d’explorateur s’ouvrira alors pour vous demander où enregistrer ce dossier ZIP. Enregistrez le, où bon vous semble.

Image du repository GitHub de l'application frontend
Repository GitHub de l'application frontend

Extraire le dossier ZIP

Ensuite il faudra extraire ce dossier ZIP, en faisant un clic droit dessus, et en choisissant “Extraire vers”.

Ouvrez ensuite ce dossier extrait et copiez tous les dossiers et fichiers qu’il contient. 

Ouvrez ensuite le dossier API_FullStack, depuis votre explorateur de fichiers. A l’intérieur, créez un nouveau dossier que vous nommez frontend, et collez y les fichiers et dossiers copiés précédemment. 

Ouvrez un terminal depuis le dossier API_FullStack, et depuis la ligne de commande, entrez dans votre nouveau dossier frontend en tapant :

				
					cd frontend

				
			

Votre ligne de commande doit maintenant se finir par …./API_FullStack/frontend/

Installer maintenant l’application frontend en tapant : 

				
					npm install
				
			

L’installation va prendre quelques temps, en fonction de votre système, de votre connexion, etc. 

Une fois l’installation terminée, lancer le serveur frontend de l’application en tapant : 

				
					ng serve
				
			

Le serveur frontend est lancé. Ouvrez votre navigateur web, et saisissez dans la barre d’adresse : 

http://localhost:4200

L’application frontend doit s’afficher correctement. 

Image de l'application frontend Beer Paradise utilisable après la création de l'API
Application frontend Beer Paradise utilisable après la création de l'API

Création du dossier backend

Pour créer le dossier backend dans lequel nous coderons l’api, j’explique pour ceux qui ont un compte GitHub et pour ceux qui ne l’ont pas : 

Pour ceux qui ont un compte GitHub :

Pour pouvoir versionner le code de notre API, créons un nouveau repository sur GitHub.

Pour ma part, je le nomme serieAW-Construire_API-Backend , mais vous pouvez le nommer comme vous le souhaitez. Copions ensuite le lien du repository que nous venons de créer.

Ensuite de retour sur VS Code, je me rends dans la ligne de commande de mon terminal, en m’assurant que je suis bien dans mon dossier API_FullStack, et je tape :

				
					git clone https://github.com/FabRiviere/serieAW-Construire_API-Backend.git backend
				
			

Après avoir taper sur la touche Entrée, un nouveau dossier backend doit être présent dans votre projet API_FullStack

Si vous n’avez pas créer de fichier .gitignore lors de la création de votre repository sur GitHub, créez en un.

À l’intérieur de ce fichier .gitignore excluons les dossiers que nous n’enverrons pas sur notre dépôt distant, en tapant :

				
					node_modules/
images/
				
			

Pour ceux qui n'ont pas de compte GitHub :

Pour créer le dossier backend, ouvrons un nouveau terminal depuis VS Code, ou depuis son système, et plaçons nous dans notre dossier API_FullStack. Nous allons taper en ligne de commande :

				
					mkdrir backend
				
			

Après avoir taper sur la touche Entrée, un nouveau dossier backend doit être présent dans votre projet API_FullStack

Le reste du contenu est commun à tous

Initialisation du projet backend

À partir de ce projet ouvrons un terminal, et rendons nous dans le dossier  backend, en tapant :

				
					cd backend
				
			

Notre ligne de commande doit maintenant se finir par ……\API_FullStack\backend> et  nous allons, depuis ce dossier backend, initialiser notre projet avec la commande :

				
					npm init
				
			

Cette commande permettra de générer un fichier package.json qui contiendra toutes les informations et toutes les dépendances de notre projet.

Dans la ligne de commande, des questions nous sont posées afin de renseigner les informations concernant notre projet. A chaque question un choix par défaut est proposé entre parenthèses.

Nous pouvons personnaliser les options (proposées dans la ligne de commande) de notre package.json, en tapant le texte ou laisser les choix par défaut. Nous validons chaque choix en tapant sur la touche Entrée.

Pour ma part, je modifie certains de ces choix. Libre à vous de laisser les choix par défaut, cependant notre point d’entrée (endpoint) sera server.js, et non pas index.js comme indiqué par défaut. 

Une fois que toutes les informations demandées depuis la ligne de commande sont validées, un résumé de ces informations sera affiché dans le terminal, et une nouvelle ligne de commande, nous demande de valider ces informations, avec comme choix par défaut (yes). Nous validons en tapant Entrée. 

Création fichier server.js

Créons ensuite dans notre dossier backend, un fichier server.js, que nous avons désigné comme point d’entrée lors de la création de notre package.json, et qui contiendra notre serveur Node. 

Démarrage d’un serveur basique

Pour créer un serveur Node dans notre fichier server.js, nous allons coder ceci :

				
					const http = require('http');
 
const server = http.createServer((req,res) => {
    res.end('Je suis la réponse du serveur Node !') 
});
 
server.listen(process.env.PORT || 3000);

				
			

Dans ce code, on commence par importer le package HTTP natif de Node, et on l’utilise en créant une fonction qui sera exécutée  à chaque appel vers le serveur.

Cette fonction reçoit les objets request et response en tant qu’arguments. Nous utilisons ensuite la méthode end de response pour renvoyer une réponse de type string

Et enfin on configure le serveur pour qu’il écoute :

  •  soit la variable d’environnement du port, grâce à process.env.PORT : si on déploie notre application chez un hébergeur, c’est le port par défaut qui sera écouté.
  • Soit le port 3000, qui nous servira dans le cas de notre plateforme de déploiement. 

Nous pouvons maintenant démarrer le serveur en exécutant depuis la ligne de commande du terminal, la commande :

				
					node server
				
			

Votre pare-feu peut vous demander une autorisation pour ouvrir les ports, ce qui est normal. 

Vérification avec Postman

Nous pouvons vérifier ensuite que notre serveur est bien lancer en ouvrant une fenêtre de navigateur à l’adresse http://localhost:3000, ou en utilisant un outil de tests tel que Postman par exemple. 

Image de la requête faite au serveur Node sur Postman
Requête HTTP faite au serveur Node sur le port 3000 depuis l'outil Postman

Nous avons ici la réponse du serveur, qui est la string (la chaine de caractères) que nous avons passer en paramètres de notre méthode end dans l’objet response

Maintenant, juste pour s’assurer que tout se passe bien, changeons cette string, donc cette chaîne de caractères en “Je suis la réponse du serveur Node.js !!” . 

Et après avoir modifier ceci, refaisons une requête dans Postman, ou sur le navigateur. 

La réponse renvoyée par Postman, ou par le navigateur, ne change pas ! Effectivement, cela est normal, car pour voir le changement, nous devons tuer notre serveur en faisant un CTRL +C dans le terminal, et exécuter de nouveau la commande node server

Et maintenant si nous faisons de nouveau notre requête sur Postman ou en réactualisant le navigateur, cela nous affiche notre string modifiée. 

Ceci va vite devenir chronophage de devoir à chaque modification, couper et relancer le serveur, vous ne pensez pas ? 

Et bien pour cela nous pouvons installer un premier package qui est Nodemon.

Installation de Nodemon

Nous devons en premier lieu, couper le serveur Node en cours (Ctrl + c), et installer le package Nodemon  depuis le terminal, en tapant la commande suivante : 

				
					npm install nodemon -g
				
			

Dans la commande ci-dessus le -g est ce que nous appelons un flag (drapeau en français) et ceci indique que nous installons Nodemon  de façon globale sur notre système. Ainsi nous pourrons l’utiliser sur tous nos autres projets. Si vous souhaitez installer Nodemon  que pour ce projet, alors ne mettez pas ce flag -g

Lancement de Nodemon

Après son installation, pour lancer notre serveur, nous utiliserons maintenant la commande :

				
					nodemon server
				
			

Celui-ci surveillera les modifications de nos fichiers et redémarrera automatiquement le serveur, pour avoir la version mise à jour, dès que nous sauvegarderons les fichiers. A ce propos, je ne sais pas pour vous , mais pour ma part, j’ai toujours l’enregistrement automatique de mes fichiers, activé sur VSCode.

Il suffit de cocher la ligne “Enregistrement automatique” se trouvant dans l’onglet fichier de VSCode.

Comme cela, je peux uniquement me concentrer sur mon code, et tous les changements se font automatiquement. 

Pour ceux qui n’ont pas d’option “Enregistrement automatique”, il ne faudra pas oublier à chaque modification, d’enregistrer le fichier modifié, en faisant un Ctrl + s , à chaque fois.

Nous pouvons vérifier en modifiant de nouveau notre chaîne de caractères de notre objet response, et nous pouvons voir que le serveur redémarre après la modification, et que maintenant, la réponse, sur Postman, ou le navigateur, est instantanée. 

Bravo, vous savez maintenant comment démarrer un serveur de développement Node.

Dans le prochain chapitre, nous verrons comment simplifier la création de notre API en ajoutant Express à notre projet. 

Chapitre 1 - Création d’une application Express

Créer des serveurs avec Node seulement est possible, mais néanmoins long et laborieux.

Il faudrait pour cela analyser chaque demande entrante. Express va nous simplifier ces tâches, en nous permettant de déployer nos api beaucoup plus facilement.

Installation de Express

Nous pouvons stopper Nodemon en faisant un Ctrl + c, ou alors ouvrir un autre terminal.

Pour ajouter Express à notre projet, nous allons, depuis notre dossier backend, installer son package avec la commande suivante : 

				
					npm install express
				
			

Après l’installation d’Express, dans notre dossier backend, on va créer un fichier app.js, où nous placerons notre application Express avec ce code : 

				
					const express = require('express');
 
const app = express();
 
module.exports = app;

				
			

Exécution de l'application Express sur le serveur Node

Revenons ensuite à notre fichier server.js, et modifions le ainsi :

				
					const http = require('http');
 
const app = require('./app');
 
app.set('port', process.env.PORT||3000);
const server = http.createServer(app);
 
server.listen(process.env.PORT || 3000);


				
			

Si nous lançons une requête depuis notre serveur maintenant, nous aurons une erreur 404, car notre application n’a aucun moyen de répondre. Nous lui avons enlevé les objets request et response

Ajoutons donc une réponse simple à notre fichier app.js, en codant ceci :

				
					const express = require('express');
 
const app = express();
 
app.use((req, res) => {
    res.json({message: 'La requête est reçue !'});
});
 
module.exports = app;

				
			

Maintenant, si nous effectuons une requête à notre serveur avec Postman, ou depuis le navigateur, nous devons recevoir un objet JSON en réponse, contenant le message que nous avons spécifié. Bien entendu, après avoir relancé le serveur, si vous l’aviez coupé. 

Ceci est parfait, et nous voyons que notre serveur Node gère correctement notre application Express. Ajoutons donc de nouvelles fonctionnalités à l’application.

Ajout de middleware

Une application Express est essentiellement une série de fonctions que l’on appelle middleware. Chaque élément de middleware reçoit les objets request et response, peut les lire, les analyser et les manipuler, si besoin.Il reçoit également la méthode next, qui permet à chaque middleware de passer à l’exécution du middleware suivant.

Comme le code est souvent plus parlant que de grands discours, modifions notre fichier app.js, comme ceci : 

				
					const express = require('express');
 
const app = express();
 
app.use((req, res, next) => {
   console.log("Requête est reçue !");
   next();
});
 
app.use((req, res, next) => {
  res.status(201);
   next();
});
 
app.use((req, res, next) => {
  res.json({message:'La requête a bien été reçue !'});
   next();
});
 
app.use((req, res, next) => {
 console.log('Réponse envoyée avec succès !');
 });
 
module.exports = app;
				
			

L’application Express, telle que nous venons de la coder, contient 4 éléments de middleware

  • Le 1er indique “Requête reçue” à la console, et passe l’exécution au suivant.
  • Le 2ème ajoute un code de statut 201 à la réponse, et passe l’exécution au suivant.
  • Le 3ème envoie la réponse au format JSON, et passe l’exécution au suivant.
  • Le dernier, indique “Réponse envoyée avec succès !” dans la console. 

Nous pouvons vérifier ces étapes en relançant notre requête depuis Postman, ou depuis le navigateur. Maintenant nous devrions avoir une réponse avec un code 201, et un objet JSON contenant le message que nous avons transmis, et dans la console du terminal, nous devrions avoir 2 nouveaux messages également. 

Parfait. Bien entendu ceci est un serveur très simple, mais cela permet d’illustrer comment fonctionne le middleware d’une application Express.

Amélioration de server.js

Avant de continuer à créer notre api, nous devons améliorer notre fichier server.js, afin de le rendre plus stable et approprié pour le déploiement. Modifions le ainsi :

				
					const http = require('http');
 
const app = require('./app');
 
const normalizePort = val => {
    const port = parseInt(val, 10);
 
    if(isNaN(port)) {
        return val;
    }
 
    if(port >= 0) {
        return port;
    }
 
    return false;
};
 
const port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
 
const errorHandler =error => {
    if(error.syscall !== 'listen') {
        throw error;
    }
 
    const address = server.address();
    const bind = typeof address === 'string'?'pipe' +address:'port:'+port;
    switch (error.code) {
        case 'EACCES':
            console.error(bind + 'requires elevated privileges.');
            process.exit(1);
            break;
        case 'EADDRINUSE':
            console.error(bind + 'is already in use.');
            process.exit(1);
            break;
        default:
            throw error;
    }
};
 
const server = http.createServer(app);
 
server.on('error', errorHandler);
server.on('listening', () => {
    const address = server.address();
    const bind = typeof address === 'string' ? 'pipe ' + address: 'port' + port;
    console.log('Listening on ' + bind);
});
 
server.listen(port);

				
			

Qu’est ce que nous venons de faire ? Nous avons créer : 

  • La fonction normalizePort, qui va renvoyer un port valide, après avoir vérifié si cela n’est pas un nombre, alors elle nous renvoie la valeur, et donc une chaîne de caractères, et si c’est un nombre alors elle renvoie ce nombre.
  • La fonction errorHandler, recherche les différents cas d’erreurs possibles, et les gère de manière appropriée.Elle est ensuite enregistrée dans le serveur. (server.on (‘error’, errorHandler)).
  • Un écouteur d’événements (server.on(‘listening’…) est également enregistré pour indiquer dans la console,  le port sur lequel le serveur s’exécute. 

Le serveur de développement Node est maintenant fonctionnel, et nous pouvons continuer à ajouter de nouvelles fonctionnalités à notre application Express.

Chapitre 2 - Configuration de la base de données MongoDB

Afin de faire persister nos données et de rendre notre application dynamique, nous allons utiliser la couche de base de données de notre serveur : MongoDB.

Pour ce tutoriel, nous utiliserons la version gratuite de MongoDB, qui est MongoDB Atlas.

Sachez qu’il est possible d’utiliser MongoDB sur votre propre machine, mais cela a un coût.

C’est pourquoi nous utiliserons ici, la base de données en tant que service, MongoDB Atlas qui est encore gratuite, à l’heure ou j’écris ce tutoriel.En espérant que cela dure…

Configuration de MongoDB Atlas

J’ai consacré un tutoriel complet sur la création d’un compte chez MongoDB, et l’utilisation de cette version MongoDB Atlas. Je ne vais donc pas réécrire ce tutoriel ici, et je vous encourage à suivre ce tutoriel présent sur le blog Activateur Web. 

Vous pouvez revenir sur cette série à partir de la section de l’article de blog intitulée “Connexion depuis éditeur de code / VSCode”. Nous allons voir cela maintenant.

Connexion de l’API au cluster MongoDB

Installation du package Mongoose

Afin de connecter notre API à notre base de données, nous aurons besoin du package Mongoose. Mongoose facilitera les interactions avec notre base de données grâce à des fonctions très utiles. 

Dans le terminal nous allons donc taper : 

				
					npm install mongoose
				
			

On tape ensuite sur la touche entrée, pour lancer le téléchargement et l’installation du package. 

Une fois l’installation terminée, nous importons mongoose dans notre fichier app.js, en ajoutant la constante suivante : 

				
					const mongoose = require('mongoose');

				
			

Juste en dessous de la constante app, nous coderons ceci afin de connecter notre base de données à notre api node :

				
					mongoose.connect('mongodb+srv://NOMUTILISATEUR:PASSWORD@cluster0.ktvxb.mongodb.net/NOMBASEDEDONNEES?retryWrites=true&w=majority',
{ useNewUrlParser: true,
   useUnifiedTopology: true })
.then(() => console.log('Connexion à MongoDB réussie !'))
.catch(() => console.log('Connexion à MongoDB échouée !'));


				
			

Bien entendu, vous devez remplacer NOMUTILISATEUR par votre nom d’utilisateur MongoDB, PASSWORD, par votre mot de passe, et NOMBASEDEDONNEES par le nom de votre base de données créée sur MongoDB

Nous vérifions si notre base de données est bien connectée, en relançant le serveur Node, et en regardant ce qui s’affiche dans notre console.

Notre console nous affiche bien “Connexion à MongoDB réussie !

En cas de Warning en ligne de commande sur Mongoose

Par contre, je ne sais de votre côté, mais pour ma part, la ligne de commande me renvoie un warning sur l’utilisation de l’option “strictQuery” dans Mongoose version 7

Image du terminal indiquant un warning pour le module mongoose
Terminal indiquant un warning pour le module mongoose

Nous pourrions ignorer ce warning. Cela ne nous empêchera en rien d’utiliser Mongoose. Malgré tout, nous pouvons aussi en prendre compte et trouver une solution.

Pour régler cela, et que ce warning ne soit plus affiché, nous allons avant la déclaration de la connexion à la base de données, coder : 

				
					mongoose.set('strictQuery', true);
				
			

Après avoir coder cela, avant la connexion à la base de données, le warning ne s’affiche plus dans notre console. 

Notre api node est maintenant connectée à notre base de données et nous pouvons donc commencer à créer des routes serveur afin d’en bénéficier. 

Chapitre 3 - Création du schéma de données Utilisateurs

L’utilisation de Mongoose pour gérer notre base de données MongoDB, nous permet de créer des schémas de données stricts. Cela rendra notre application plus robuste.

Commençons donc à implémenter un schéma User, pour les utilisateurs de notre application. 

Création du schéma User

Dans notre dossier backend, nous allons créer un nouveau dossier que nous nommons models. Et dans ce nouveau dossier, nous créons un fichier User.js

Par convention, nous mettons une majuscule au nom de notre fichier de modèle.

Utilisateur unique

Pour s’assurer que 2 utilisateurs ne peuvent pas utiliser la même adresse email, nous utiliserons le mot clé unique pour l’attribut email du schema userSchema.

Les erreurs générées par MongoDB pouvant être difficiles à résoudre, nous installerons un package de validation pour pré-valider les informations avant de les enregistrer : 

Ce package est le package mongoose-unique-validator. Nous l’installons donc en tapant dans un nouveau terminal : 

				
					npm install mongoose-unique-validator
				
			

Après l’installation de ce package, créons notre modèle utilisateur.

Dans notre fichier User.js, nous importons Mongoose, ainsi que notre package nouvellement installé :

				
					const mongoose = require('mongoose');
const uniqueValidator = require('mongoose-unique-validator');
				
			

Puis nous codons notre schéma ainsi : 

				
					const userSchema = mongoose.Schema({
    email: { type: String, required: true, unique: true },
    password: { type: String, required: true }
});
 
userSchema.plugin(uniqueValidator);
 
module.exports = mongoose.model('User', userSchema);

				
			

Ce que nous faisons en écrivant ce code

  • Grâce à la méthode Schema mise à disposition par Mongoose, on crée un schéma de données qui contient les champs souhaités pour chaque utilisateur, indiquant leur type ainsi que leur caractère obligatoire ou non. 
    • A noter que nous n’avons pas mis de champ pour l’id, puisqu’il est automatiquement généré par Mongoose
    • Dans notre schéma, la valeur unique, avec l’élément mongoose-unique-validator passé comme plugin, s’assurera qu’aucun des utilisateurs, ne peut partager la même adresse email. 
  • On exporte ce schéma en tant que modèle Mongoose, et on le nomme User.

Ce modèle nous permet d’appliquer une structure de données, mais nous simplifiera également les opérations de lecture et d’écriture que nous verrons par la suite. 

Notre modèle User est maintenant prêt et nous allons pouvoir l’exploiter pour enregistrer de nouveaux utilisateurs dans la base de données et appliquer le chiffrement de mot de passe

Comprendre le stockage de mot de passe sécurisé

Dans la suite de ce tutoriel, nous mettrons en place l’authentification par e-mail et mot de passe pour notre api. 

Cela implique que nous devons stocker les mots de passe de nos utilisateurs dans notre base de données. Bien entendu, nous ne pouvons pas stocker ces mots de passe sous forme de texte brut. Imaginez que quelqu’un accède à notre base de données, et il aurait accès à toutes les données de nos utilisateurs. Nous devons donc stocker ces mots de passe sous la forme d’un hash ou d’une chaîne chiffrée

Pour faire cela, nous utiliserons un package de chiffrage qui est bcrypt. Celui-ci utilise un algorithme unidirectionnel pour chiffrer et créer un hash des mots de passe utilisateur, et que nous stockerons ensuite dans le document de la base de données relatif à chaque utilisateur.

Lorsqu’un de nos utilisateur tentera de se connecter, nous utiliserons bcrypt pour créer un hash avec le mot de passe entré, puis nous le comparerons au hash stocké dans la base de données. 

Mais là encore, afin d’éviter d’éventuels piratages, ces deux hash ne seront pas les mêmes.

Le package bcrypt permet d’indiquer si les 2 hash, différents donc, ont été générés à l’aide d’un même mot de passe initial. Il nous aidera donc à implémenter correctement le stockage et la vérification sécurisée des mots de passe. Nous pouvons même ajouter quelques sels…

La première étape pour implémenter notre authentification est de créer un modèle de base de données pour les informations de nos utilisateurs

Alors prêt ? 

Création des utilisateurs

Pour pouvoir utiliser notre nouveau modèle dans l’application, nous devons l’importer dans notre fichier app.js

				
					
const User = require('./models/User');
				
			

Création des utilisateurs de façon sécurisée

Comme dit précédemment, nous devons hasher nos mots de passe, et pour cela nous devons donc installer le package bcrypt . Dans notre terminal nous tapons donc : 

				
					npm install bcrypt
				
			

Après l’installation de ce package, nous l’importons dans notre fichier app.js :

				
					const bcrypt = require('bcrypt');
				
			

Nous pouvons supprimer ou, au minimum mettre en commentaires, nos middlewares créés lors de l’implémentation de notre serveur.

Route POST - Inscription des utilisateurs

Nous pouvons créer la logique de notre route POST et implémenter notre middleware pour l’inscription des utilisateurs. 

Nous codons donc comme ceci : 

				
					app.post('/api/auth/signup', (req, res, next) => {
 
   bcrypt.hash(req.body.password, 10)
        .then(hash => {
            const user = new User({
                email: req.body.email,
                password: hash
            });
            user.save()
                .then(() => res.status(201).json({ message: 'Utilisateur créé avec succès !' }))
                .catch(error => res.status(400).json({ error }));
        })
        .catch(error => res.status(500).json({ error }));
 
});

				
			

Que faisons nous dans le middleware ci -dessus

  • Nous appelons la méthode hash de bcrypt, et nous lui demandons en paramètre de “saler” le mot de passe 10 fois. A noter que plus la valeur du salage est élevée, plus l’exécution de la fonction sera longue, et plus le hachage sera sécurisé. En ajoutant la valeur de “salage” à 10, le mot de passe sera déjà bien sécurisé.
  • Cette fonction asynchrone renvoie une Promise (promesse), dans laquelle nous recevons le hash généré. 
  • Dans le bloc then, nous créons et enregistrons l’utilisateur dans la base de données, et dans ce bloc then, nous créons un autre bloc then, et un bloc catch, ou nous renvoyons une réponse de réussite avec le code de statut 201, ou en cas d’échec, un code 400 ainsi que l’erreur générée. 
  • Dans le bloc catch, en cas d’échec du routage ou du middleware ,nous renvoyons une erreur avec le code 500.

Test de la route POST des utilisateurs

Pour tester notre middleware, nous pouvons utiliser Postman.

Dans Postman, je commence par créer un nouveau “Workspace” ou espace de travail en français, que je nomme, par exemple, API_FullStack. Dans cet espace de travail, je crée maintenant un nouveau dossier que je nomme Users Schema. Dans ce nouveau dossier, je créais ensuite une nouvelle requête que je nomme User Signup.

Cette requête sera donc de type POST, et nous lui passons l’adresse (appelé endpoint ou point de terminaison) définie dans notre middleware, qui sera donc http://localhost:3000/api/auth/signup .

Ensuite je vais dans l’onglet “body”, cocher le bouton radio x-www-form-urlencoded et ensuite renseigner les champs de paires clé/valeur. Dans le premier champ, je rentre donc email pour la “key”, et une adresse email pour la “value”. Dans un deuxième champ, je saisi “password” comme “key”, et un mot de passe dans le champ “value”. Je clique ensuite sur le bouton “Send” pour envoyer la requête. Voici ce qui se passe : 

Image de l'erreur reçue dans Postman
Erreur reçue dans Postman

Postman me renvoie une erreur avec le code 500.

Test de la route POST depuis le frontend de l’application.

Je peux également essayer de tester le middleware coder sur l’application frontend. Sur l’application frontend ouverte dans mon navigateur, je clique tout d’abord sur le bouton “Signup” situé dans le header. Ensuite, je renseigne les champs email et  password. 

Et là aussi, une erreur est renvoyée : 

Image de l'erreur reçue sur le frontend de l'application
Erreur reçue sur le frontend de l'application

Extraire les objets JSON correctement

L’erreur qui se produit ici, est que pour gérer la demande POST qui provient de l’application frontend, nous devons être en capacité d’extraire l’objet JSON de la demande.

Depuis quelques temps maintenant, Express intègre nativement le package body-parser.

Avant, nous devions installer ce package body-parser, mais étant donné que Express l’intégre nativement, nous devons simplement l’implémenter dans notre fichier app.js. Après la connexion à la base de données, nous codons donc ceci :

				
					app.use(express.json());
app.use(express.urlencoded({ extended: true }));

				
			

Nouveaux tests avec Postman et le frontend

Refaisons maintenant nos tests d’inscription utilisateurs. 

Sur Postman : 

Image de la réponse avec succès dans Postman de l'inscription d'un utilisateur
Réponse avec succès dans Postman de l'inscription d'un utilisateur

Je reçois maintenant une réponse avec le code de statut 201, et le message de succès “Utilisateur créé avec succès !

Avec le frontend : 

Si vous avez fait le test déjà avec Postman, rappelez vous que l’email doit être unique et il faudra donc ne pas mettre la même adresse :

Image d'erreur dans Postman même après implémentation de l'encodage en JSON
Nouvelle erreur dans Postman même après implémentation de l'encodage en JSON

Sur le frontend, je reçois toujours une erreur ?? 

Comment est ce possible puisque Postman lui me répond favorablement ? 

Servons nous de nos outils de navigateur pour comprendre cette erreur. 

Pour ma part, j’utilise Google Chrome, mais les outils sont maintenant présents sur tous les navigateurs. Pour ouvrir ces outils, je tape sur la touche F12 de mon clavier, ou je fais un clic droit, et je sélectionne  “Inspecter” . Je vais ensuite sur l’onglet Console

Image des outils de navigateur avec affichage dans la console d'erreurs de CORS
Outils de navigateur avec affichage dans la console d'erreurs de CORS

Et bien nous avons à faire ici à une erreur de CORS

Erreurs de CORS

CORS signifie “Cross Origin Resource Sharing”. C’est un système de sécurité qui, par défaut, bloque les appels HTTP effectués entre des serveurs différents, et donc les requêtes malveillantes d’accéder à des ressources sensibles. Dans notre cas, lorsque nous utilisons Postman, la requête vient de la même origine : localhost:3000. Dans ce cas, pas d’erreur. Lorsque par contre nous utilisons le front end, nous avons 2 origines : localhost:3000 et localhost:4200, et cette fois cela produit une erreur. 

Afin de faire communiquer ces 2 origines entre elles, nous devons ajouter des headers à notre objet “response”.

Dans notre fichier app.js, après la connexion à la base de données, codons le middleware suivant comme ceci :

				
					app.use((req, res, next) => {
   res.setHeader('Access-Control-Allow-Origin', '*');
   res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content, Accept, Content-Type, Authorization');
   res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
   res.setHeader('Access-Control-Allow-Credentials', true);
   next();
});

				
			

Ces headers permettent : 

  • D’accéder à notre API depuis n’importe quelle origine (‘*’).
  • D’ajouter les headers mentionnés aux requêtes envoyées vers notre API (Origin, X-Request-With, etc. 
  • D’envoyer des requêtes avec les méthodes mentionnées (GET,POST,PUT, etc.).

Vous aurez peut être noté que nous n’avons pas mis d’adresse en premier paramètre de ce middleware. Il s’appliquera ainsi à toutes les routes.

Il nous reste à refaire un test depuis notre frontend

Nouveau test avec le frontend

J’actualise ma page de navigateur et je renseigne donc de nouveau les champs, et clique ensuite sur le bouton Sign up.

Image sur le frontend d'une erreur car la route login n'existe pas encore
Sur le frontend, nouvelle erreur mais cette fois, à cause de la route "login" qui n'existe pas

Cette fois, je reçois une nouvelle erreur mais qui n’est pas la même qu’auparavant , et qui m’indique une erreur sur la route “login”. Normal puisque nous n’avons pas encore implémenter ce middleware. 

Par contre, nous pouvons aller voir sur notre base de données sur MongoDB, et nous voyons que notre utilisateur a bien été créer : 

Image de la base de données MongoDB avec création d'un utilisateur depuis le frontend
Base de données MongoDB avec enregistrement d'un utilisateur depuis le frontend

Nous pouvons voir également que le mot de passe utilisateur a bien été haché et donc indéchiffrable

Nous pouvons donc en déduire que notre middleware d’inscription et notre route POST fonctionnent bien. 

Nous pouvons poursuivre avec l’implémentation d’un nouveau middleware, qui permettra de vérifier les informations d’identification des utilisateurs pour se connecter

Vérifier les informations d’identification d’un utilisateur

Implémentation du middleware de login

Maintenant que les utilisateurs peuvent s’inscrire grâce à notre middleware signup créé précédemment, et que nous enregistrons leurs données en base de données, il nous faut un moyen de vérifier qu’un utilisateur qui tente de se connecter, possède des identifiants valides. Pour cela, codons donc notre middleware de login :

				
					app.post('/api/auth/login', (req, res, next) => {
 
   User.findOne({ email: req.body.email })
        .then(user => {
            if(!user) {
                return res.status(401).json({ error: "L\'utilisateur n\'existe pas ! "});
            }
            bcrypt.compare(req.body.password, user.password)
                .then(valid => {
                    if(!valid) {
                        return res.status(401).json({ error: 'Mot de passe incorrect !' });
                    }
                    res.status(200).json({
                        userId: user._id,
                        token: 'TOKEN'
                    });
                })
                .catch(error => res.status(500).json({ error }));
        })
        .catch(error => res.status(500).json({ error }));
 
});

				
			

Que faisons nous dans ce middleware ? : 

  • Nous utilisons la méthode findOne de Mongoose pour vérifier si l’email saisi par l’utilisateur correspond à l’email d’un utilisateur existant dans la base de données.
    • Si l’email n’existe pas nous renvoyons une erreur avec code 401.
    • Si l’email correspond à un email existant, on continue. 
  • Nous utilisons la fonction compare de bcrypt pour comparer le mot de passe saisi par l’utilisateur avec le hash enregistré dans la base de données.
    • S’ils ne correspondent pas, nous renvoyons une erreur 401 “Unauthorized”, et un message “Mot de passe incorrect”.
    • S’ils correspondent, les informations d’identification de notre utilisateur sont valides. Dans ce cas, nous renvoyons une réponse 200 contenant l’ID utilisateur et un token. Ce token est une chaîne générique pour l’instant, mais nous allons le modifier et le crypter dans la section suivante. 

Création de tokens d’authentification

Les tokens d’authentification permettent aux utilisateurs de ne se connecter qu’une seule fois à leur compte. En effet, lorsqu’un utilisateur se connecte, il reçoit un token qui sera renvoyé automatiquement à chaque requête faite par la suite par cet utilisateur.

Ceci permettra au back-end de vérifier que la requête est authentifiée. 

Pour pouvoir créer et vérifier les tokens d’authentification, nous aurons besoin d’un nouveau package qui est jsonwebtoken

Installons donc ce package en tapant dans notre terminal : 

				
					npm install  jsonwebtoken
				
			

Après l’installation du package, nous l’importons, bien entendu, dans notre fichier app.js :

				
					const jwt = require('jsonwebtoken');

				
			

Ensuite, mettons le en œuvre dans notre middleware login, en remplaçant la chaîne de caractère générique TOKEN, que nous avions mise en attendant :

				
					app.post('/api/auth/login', (req, res, next) => {
 
   User.findOne({ email: req.body.email })
   .then(user => {
       if(!user) {
           return res.status(401).json({ error: "L\'utilisateur n\'existe pas ! "});
       }
       bcrypt.compare(req.body.password, user.password)
           .then(valid => {
               if(!valid) {
                   return res.status(401).json({ error: 'Mot de passe incorrect !' });
               }
               res.status(200).json({
                   userId: user._id,
                   token: jwt.sign(
                       { userId: user._id },
                       'RANDOM_TOKEN_SECRET',
                       { expiresIn: '24h' }
                   )
               });
           })
           .catch(error => res.status(500).json({ error }));
   })
   .catch(error => res.status(500).json({ error }));
 
});
				
			

Dans le code ci-dessus : 

  • Nous utilisons la fonction sign de jsonwebtoken pour encoder un nouveau token.
  • Ce token contient l’ID de l’utilisateur en tant que payload (les données encodées dans le token).
  • Nous utilisons une chaîne secrète de développement temporaire RANDOM_SECRET_KEY pour encoder notre token (à remplacer par une chaîne aléatoire beaucoup plus longue pour la production).
  • Nous définissons la durée de validité du token à 24 heures. L’utilisateur devra donc se reconnecter au bout de 24 heures.
  • Nous renvoyons le token au front-end avec notre réponse.

Maintenant lorsqu’un utilisateur se connecte, chaque requête provenant du front end contient un en-tête “Authorization” avec le mot clé Bearer suivi d’une longue chaîne encodée. Cette chaîne est notre token.

Nous pouvons le vérifier soit dans l’onglet réseau de nos outils de navigateur ou également sur notre outil Postman

Test dans Postman :

Pour tester mon nouveau middleware login, je créé une nouvelle requête de type POST sur le endpoint http://localhost:3000/api/auth/login et dans les champs du body, je saisi les champs email et password en utilisant l’adresse email et le mot de passe avec lequel je me suis inscrit, lors du test du middleware signup. 

Image du test du middleware "login" sur Postman
Test du middleware "login" sur Postman

Je reçois une réponse de succès avec un code de statut 201, et dans la réponse le userId et le token d’authentification.

Test sur le frontend :

Sur l’application frontend, je clique sur le bouton Login situé dans le header de l’application.

Une fois sur l’adresse login, je rentre l’email et le mot de passe avec lesquels je me suis inscrit auparavant, et cliques sur le bouton Login

Image du test du middleware "login" sur le frontend
Test du middleware "login" sur le frontend

Cette fois, nous voyons que l’authentification fonctionne et je suis redirigé vers l’adresse “beers”. Pour l’instant, nous avons juste un spinner qui s’affiche. Cela est normal puisque nous n’avons rien coder pour le moment pour les routes menant aux avis sur les bières. 

Mais l’important ici est de voir que notre middleware login fonctionne

Pour sécuriser notre application, dans les prochaines sections, nous allons devoir créer un middleware pour vérifier ce token et son contenu, afin de nous assurer que seules les requêtes autorisées ont accès aux routes à protéger. 

Nous verrons cela par la suite, car pour l’heure nous devons nous pencher sur la structure de notre code, et l’organisation à mettre en place pour garantir un code lisible, robuste et maintenable.

Chapitre 4 - Optimisations de la structure du code de notre API

Organisation et structure du code

A ce stade de notre projet, il est important de rester organisé et également de penser à rendre notre code robuste, lisible et maintenable

Nous pourrions continuer à écrire nos routes et nos logiques métiers dans notre fichier app.js. Mais comme nous allons implémenter d’autres CRUD dans notre API, nous allons très vite nous retrouver à écrire de nombreuses lignes de code, et si nous codons tout cela dans notre app.js, cela deviendra vite difficile à lire et à maintenir. 

Et même si nous aurions implémenté uniquement le CRUD User que nous avons réalisé dans le chapitre précédent, il est bien de prendre l’habitude d’organiser vos projets et votre code, et d’avoir ainsi une structure, facilement compréhensible et gérable.

Pour cela, rendons les choses un peu plus modulaires. 

Configuration du routage

La première chose que nous allons faire est de séparer notre logique de “routing” et la logique globale de l’application.

Dans notre dossier backend, nous créons donc un nouveau dossierroutes”, et à l’intérieur de ce dossier nous créons un fichier users.js. Celui -ci contiendra la logique de nos routes users.

Dans ce nouveau fichier users.js, nous allons créer un routeur Express. Pour cela , voici ce que nous codons :

				
					const express = require('express');
const router = express.Router();
 
 
 
module.exports = router;
				
			

Nous écrirons notre code après notre constante router. 

Et la première chose est d’importer notre modèle User

				
					const express = require('express');
const router = express.Router();
 
const User = require('../models/User');
 
module.exports = router;

				
			

Ensuite nous allons couper les routes de notre fichier app.js, et les coller dans notre routeur. Nous devons également remplacer les occurrences de app par router, car maintenant les routes sont enregistrées dans notre routeur.

Nous devons également supprimer nos endpoints /api/auth/ de chaque segment de route. Et si cela supprime une chaîne de route, nous laisserons un slash à la place /.

 Voici donc à quoi ressemble mon fichier users.js dans le dossier routes :

				
					const express = require('express');
const router = express.Router();
 
const User = require('../models/User');
 
router.post('/signup', (req, res, next) => {
    bcrypt.hash(req.body.password, 10)
            .then(hash => {
                const user = new User({
                    email: req.body.email,
                    password: hash
                });
                user.save()
                    .then(() => res.status(201).json({ message: 'Utilisateur créé avec succès !' }))
                    .catch(error => res.status(400).json({ error }));
            })
            .catch(error => res.status(500).json({ error }));
});
 
router.post('/login', (req, res, next) => {
    User.findOne({ email: req.body.email })
    .then(user => {
        if(!user) {
            return res.status(401).json({ error: "L\'utilisateur n\'existe pas ! "});
        }
        bcrypt.compare(req.body.password, user.password)
            .then(valid => {
                if(!valid) {
                    return res.status(401).json({ error: 'Mot de passe incorrect !' });
                }
                res.status(200).json({
                    userId: user._id,
                    token: jwt.sign(
                        { userId: user._id },
                        'RANDOM_TOKEN_SECRET',
                        { expiresIn: '24h' }
                    )
                });
            })
            .catch(error => res.status(500).json({ error }));
    })
    .catch(error => res.status(500).json({ error }));
});
 
module.exports = router;

				
			

Comme nous avons déplacé nos middlewares dans notre routeur, nous devons pour éviter les erreurs, déplacer les importations des packages bcrypt et jsonwebtoken également dans ce fichier users.js

				
					//: Importation de bcrypt
const bcrypt = require('bcrypt');
//: Importation de jsonwebtoken
const jwt = require('jsonwebtoken');

				
			

Importation du routeur dans app.js

Notre routeur est maintenant créé, mais pour l’utiliser dans notre application nous devons l’enregistrer dans notre fichier app.js

Nous devons tout d’abord l’importer, et nous en profitons pour supprimer l’importation de notre modèle dans le fichier app.js, et à la place codons l’importation de notre routeur :

				
					//: Importation du routeur User
const userRoutes = require('./routes/users');

				
			

Nous devons ensuite l’enregistrer comme nous le ferions pour une route unique. Nous enregistrons notre routeur pour que toutes les demandes effectuées soient sur  le endpoint  “/api/auth”. On code donc :

				
					app.use('/api/auth', userRoutes);

				
			

A ce stade, nos routes doivent fonctionner comme auparavant. Nous pouvons tester cela dans notre outil Postman ou sur le frontend de l’application.

Configuration des contrôleurs

Pour rendre notre structure encore plus modulaire, simplifier la lecture et la gestion de notre code, nous allons séparer la logique métier de nos routes en contrôleurs.

Nous créons donc un dossier controllers dans notre dossier backend, et nous créons à l’intérieur de ce nouveau dossier, un fichier user.js.

La première chose à faire dans ce fichier est d’importer notre modèle : 

				
					const User = require('../models/User');
				
			

Nous allons ensuite copier le 1er élément de logique métier de la route POST vers notre contrôleur. Pour cela, dans routes/users.js, nous coupons le code à partir de notre accolade d’ouverture (req, res, next) et jusqu’à l’accolade fermante :

Image de la partie du code de notre logique métiers à couper et à déplacer dans les contrôleurs
Partie du code de notre logique métiers à couper et à déplacer dans les contrôleurs

et dans notre fichier controllers/user.js, nous créons une fonction que nous nommons signup, par exemple, et comme nous allons l’exporter nous écrivons exports.signup = et nous collons le code couper auparavant : 

				
					exports.signup = (req, res, next) => {
    bcrypt.hash(req.body.password, 10)
            .then(hash => {
                const user = new User({
                    email: req.body.email,
                    password: hash
                });
                user.save()
                    .then(() => res.status(201).json({ message: 'Utilisateur créé avec succès !' }))
                    .catch(error => res.status(400).json({ error }));
            })
            .catch(error => res.status(500).json({ error }));
};

				
			

Ici nous exposons la logique de notre route en tant que fonction appelée signup

Pour nous servir de cette fonction, nous devons également déplacer l’implémentation de nos packages bcrypt et jsonwebtoken. Nous les coupons donc de notre fichier de routes et le déplaçons dans notre contrôleur.

				
					const User = require('../models/User');
//: Importation de bcrypt
const bcrypt = require('bcrypt');
//: Importation de jsonwebtoken
const jwt = require('jsonwebtoken');
 
exports.signup = (req, res, next) => {
    bcrypt.hash(req.body.password, 10)
            .then(hash => {
                const user = new User({
                    email: req.body.email,
                    password: hash
                });
                user.save()
                    .then(() => res.status(201).json({ message: 'Utilisateur créé avec succès !' }))
                    .catch(error => res.status(400).json({ error }));
            })
            .catch(error => res.status(500).json({ error }));
};

				
			

Pour implémenter cela dans notre route, nous devons importer notre contrôleur puis enregistrer signup. Dans notre fichier routes/users.js, nous faisons cela ainsi : 

				
					const userCtrl = require('../controllers/user');
 
router.post('/signup', userCtrl.signup);
 

				
			

Nous pouvons donc faire de même pour la route login

Voici le contrôleur final controllers/user.js

				
					//: Importation du modèle
const User = require('../models/User');
//: Importation de bcrypt
const bcrypt = require('bcrypt');
//: Importation de jsonwebtoken
const jwt = require('jsonwebtoken');
 
exports.signup = (req, res, next) => {
    bcrypt.hash(req.body.password, 10)
            .then(hash => {
                const user = new User({
                    email: req.body.email,
                    password: hash
                });
                user.save()
                    .then(() => res.status(201).json({ message: 'Utilisateur créé avec succès !' }))
                    .catch(error => res.status(400).json({ error }));
            })
            .catch(error => res.status(500).json({ error }));
};
 
exports.login = (req, res, next) => {
    User.findOne({ email: req.body.email })
    .then(user => {
        if(!user) {
            return res.status(401).json({ error: "L\'utilisateur n\'existe pas ! "});
        }
        bcrypt.compare(req.body.password, user.password)
            .then(valid => {
                if(!valid) {
                    return res.status(401).json({ error: 'Mot de passe incorrect !' });
                }
                res.status(200).json({
                    userId: user._id,
                    token: jwt.sign(
                        { userId: user._id },
                        'RANDOM_TOKEN_SECRET',
                        { expiresIn: '24h' }
                    )
                });
            })
            .catch(error => res.status(500).json({ error }));
    })
    .catch(error => res.status(500).json({ error }));
};

				
			

Et également notre routeur final routes/users.js

				
					const express = require('express');
const router = express.Router();
 
const userCtrl = require('../controllers/user');
 
router.post('/signup', userCtrl.signup);
 
router.post('/login', userCtrl.login);
 
module.exports = router;

				
			

Code lisible et structuré

Comme vous le voyez, notre fichier de routeur est très compréhensible. Nous voyons tout de suite les routes disponibles et à quels points de terminaison (endpoints), et les noms descriptifs donnés aux fonctions de notre contrôleur permettent de comprendre la fonction de chaque route. 

Nous pouvons, comme d’habitude tester toutes ses nouvelles fonctions sur notre outil Postman.

Structurer son code de façon modulaire comme nous venons de le faire est une très bonne habitude à prendre, car cela simplifiera sa compréhension et sa maintenance. 

Maintenant que tout est prêt, nous allons pouvoir implémenter le CRUD de nos avis de bières.

Chapitre 5 Création du schéma de données Bières

Comme nous l’avons fait pour notre modèle User, nous devons pour pouvoir gérer les avis laissés par les utilisateurs sur les bières, implémenter notre schéma Beer.

Création du schéma Beer

Dans notre dossier models, nous allons créer un nouveau fichier Beer.js

Par convention, nous mettons une majuscule au nom de notre fichier de modèle.

Nous codons ceci à l’intérieur de ce fichier models/Beer.js :

				
					const mongoose = require('mongoose');
 
const beerSchema = mongoose.Schema({
    userId: { type: String, required: true },
    name: { type: String, required: true },
    manufacturer: { type: String, required: true },
    description: { type: String, required: true },
    mainIngredient: { type: String, required: true },
    imageUrl: { type: String, required: false },
    degree : { type: Number, required: true },
    likes: { type: Number, required: true },
    dislikes: { type: Number, required: true },
    usersLiked: { type: Array, required: true },
    usersDisliked: { type: Array, required: true },
});
 
module.exports = mongoose.model('Beer', beerSchema);
				
			

Ce que nous faisons en écrivant ce code : 

  • Grâce à la méthode Schema mise à disposition par Mongoose, on crée un schéma de données qui contient les champs souhaités pour chaque bière, indiquant leur type ainsi que leur caractère obligatoire ou non. 
    • A noter que nous n’avons pas mis de champ pour l’id, puisqu’il est automatiquement généré par Mongoose
    • Nous mettons le champ image non requis pour le moment mais nous changerons par la suite. 
  • On exporte ce schéma en tant que modèle Mongoose, et on le nomme Beer.

Ce modèle nous permet d’appliquer une structure de données, mais nous simplifiera également les opérations de lecture et d’écriture que nous verrons par la suite. 

Implémenter le CRUD

Au fait, savez vous ce que signifie CRUD ?

CRUD pour : 

  • Create (création de ressources);
  • Read (lecture des ressources);
  • Update (modification des ressources);
  • Delete (suppression des ressources);

Le CRUD implémenté, permettra ainsi un parcours utilisateur complet. 

Lorsque nous parlons de CRUD pour notre application d’avis de bières, cela signifie qu’il nous faudra mettre en place : 

Create

Nous aurons besoin de créer une fonction où chaque utilisateur inscrit sur le site pourra créer un nouvel avis sur une bière.

Read

Nous devrons pouvoir afficher la liste des avis de bière publiés, et également afficher un avis de bière spécifique. 

Update

Il faudra également donner la possibilité à l’utilisateur qui a publié un avis sur une bière, de pouvoir modifier cet avis. A noter que seul l’utilisateur ayant publié cet avis sera à même de le modifier et pas les autres utilisateurs. 

Nous implémenterons un système de Like/Dislike pour chaque utilisateur, mais chaque utilisateur ne pourra liker ou disliker qu’une seule fois, tout en pouvant modifier son choix à tout moment. Le système sera alors mis à jour en temps réel..

Delete

Et enfin, comme pour la modification d’un avis, nous donnerons la possibilité à l’utilisateur ayant publié un avis de pouvoir le supprimer. Là aussi, lui seul aura cette possibilité. 

Vous le voyez, il nous reste beaucoup de travail à faire. Alors continuons…

Création de l’avis sur une bière

Commençons ce CRUD en implémentant une fonction pour la création de l’avis sur une bière. 

Nous allons donc créer un nouveau fichier beer.js dans notre dossier controllers

Dans ce nouveau fichier de contrôleur beer.js, nous devons importer notre modèle :

				
					const Beer = require ('../models/Beer');
				
			

Nous codons ensuite une fonction, que je décide pour ma part de nommer createBeer()

				
					//; Créer (ajouter) une bière
exports.createBeer = (req, res, next) => {
    const beerObject = req.body;
    delete beerObject._id;
     const beer = new Beer({
        ...beerObject,
        likes: 0,
        dislikes: 0
     });
     beer.save()
        .then(() => res.status(201).json({ message: 'Objet enregistré avec succès !' }))
        .catch(error => res.status(400).json({ error }));
  };

				
			

Ce que nous faisons dans cette fonction : 

  • Nous déclarons une constante représentant le corps de la requête.
  • Ensuite, nous supprimons l’éventuel faux _id, envoyé par le front end,
  • Puis nous créons une instance de notre modèle Beer, en lui passant un objet JavaScript contenant toutes les informations requises du corps de requête analysé. 
    • L’opérateur spread( … ) est utilisé pour faire une copie de tous les éléments de req.body parser.
  • Nous définissons les champs likes et dislikes auxquels nous mettons comme valeurs par défaut à 0.
    • Concernant l’image, nous ne l’implémentons pas pour le moment et nous en parlerons plus en détails dans le chapitre dédié aux ajouts de fichiers. 
  • Nous passons à cette nouvelle instance, la méthode save() qui enregistre notre nouveau Beer dans la base de données.
    • Cette méthode save() renvoie une Promise (promesse), avec un bloc then() qui nous renverra une réponse de réussite et un code de statut 201, et un bloc catch() qui au contraire en cas d’erreur nous renverra une erreur générée par Mongoose avec un code d’erreur 400.

Cette fonction codée, nous pouvons maintenant construire la route permettant d’envoyer nos données. 

Route POST pour la création d’un avis

Nous avons besoin au préalable, de créer un nouveau fichier ou nous allons gérer toutes les routes liées à nos avis de bières. Dans le dossier routes, je crée donc un nouveau fichier que je nomme beers.js

Dans ce fichier, je dois tout d’abord implémenter mon routeur Express, et également importer mon contrôleur : 

				
					const express = require('express');
const router = express.Router();
 
const beerCtrl = require('../controllers/beer');
 
module.exports = router;

				
			

J’implémente ensuite ma route POST, avec comme endpoint la racine de mes routes” beers”, et lui passe comme second attribut, ma fonction de contrôleur : 

				
					const express = require('express');
const router = express.Router();
 
const beerCtrl = require('../controllers/beer');
 
//: Route pour créer ou ajouter un avis de bière
router.post('/', beerCtrl.createBeer );
 
module.exports = router;
				
			

Je dois maintenant importer mon routeur dans l’application. Dans mon fichier app.js, je commence donc par importer mon routeur beers :

				
					//: Importation du routeur Beer
const beerRoutes = require('./routes/beers');
				
			

Et je déclare ensuite ma route : 

				
					app.use('/api/beers', beerRoutes);

				
			

La possibilité d’ajouter un avis sur une bière est maintenant bien avancée. Cependant, vous vous rappelez que dans les spécifications techniques et fonctionnelles, nous devons mettre en place, la sécurisation de ces routes, et par la suite, nous devrons faire en sorte que seul l’utilisateur qui a créé l’avis pourra le modifier, et  le supprimer.

Pour cela, nous devons créer un middleware d’authentification

Configuration du middleware d’authentification

Implémentation du middleware d’authentification

Nous allons maintenant créer le middleware qui protégera les routes sélectionnées et vérifier que l’utilisateur est authentifié avant d’autoriser l’envoi de ses requêtes.

Par soucis d’organisation et de continuer à structurer notre code, nous allons créer un nouveau dossier que nous nommons middleware. A l’intérieur de ce nouveau dossier, créons un nouveau fichier auth.js

Codons ensuite ceci à l’intérieur de ce fichier auth.js

				
					const jwt = require('jsonwebtoken');
 
module.exports = (req, res, next) => {
    try {
        const token = req.headers.authorization.split(' ')[1];
        const decodedToken = jwt.verify(token, process.env.TOKEN_KEY);
        const userId = decodedToken.userId;
 
        if( req.body.userId && req.body.userId !== userId ) {
            throw 'User ID non valable !';
        } else {
            next();
        }
    } catch {
        res.status(401).json({ error: error | 'Requête non authentifiée !' });
    }
};

				
			

Dans ce middleware : 

  • Nous importons le package jsonwebtoken.
  • Etant donné que de nombreux problèmes peuvent se produire, nous insérons tout notre code à l’intérieur d’un bloc trycatch
  • Nous extrayons le token du header Authorization de la requête entrante. Comme ce token contient le mot clé Bearer, nous utilisons la fonction split(), pour récupérer tout ce qu’il y a après l’espace dans le headers. Si nous rencontrons des erreurs, nous les renvoyons avec le bloc catch. 
  • Nous exploitons ensuite la fonction verify() pour décoder notre token. Si celui-ci n’est pas valide, une erreur sera générée. 
  • Nous extrayons ensuite l’ID utilisateur de notre token.
  • Si la demande contient un ID utilisateur, nous le comparons à celui extrait du token. S’ils sont différents, nous générons une erreur. 
  • Dans le cas contraire, tout fonctionne et notre utilisateur est authentifié. Nous passons l’exécution à l’aide de la fonction next()

Félicitations ! Vous venez de créer votre middleware d’authentification

Cependant, il nous faut maintenant appliquer ce middleware aux routes que nous souhaitons protéger. 

Pour faire cela, dans notre routeur beers.js, nous devons importer ce middleware, et ensuite le passer en argument aux routes à protéger. 

Dans mon fichier routes/beers.js, j’importe le middleware :

				
					const auth = require('../middleware/auth');
				
			

Et je le passe ensuite en argument aux routes à protéger. Pour le moment seule ma route POST existe, je passe donc ce middleware en attribut ainsi : 

				
					router.post('/',auth, beerCtrl.createBeer );
				
			

Notre api implémente à présent l’authentification par token et est correctement sécurisée.

Parfait. 

Il est temps de tester cette nouvelle fonction d’authentification, vous ne pensez pas ?

Allons faire cela !

Test de la fonction createBeer() avec Postman

Nous pouvons tester cette fonction avec Postman uniquement, car le frontend lorsque l’on créer un nouvel avis de bière, attends tous les champs déterminés dans le modèle, et comme nous n’avons pas encore implémenté la possibilité de télécharger les fichiers, nous utiliserons donc Postman pour nos tests. 

Dans Postman, j’ai créé un nouveau dossier que je nomme Beers Schema, par exemple. 

Je crée ensuite une nouvelle requête de type POST, avec le endpoint http://localhost:3000/api/beers . Comme nous avons implémenter notre système d’authentification, nous devons dans l’onglet “Authorization” , sélectionner le type sur “Bearer Token”, et après avoir relancer ma requête Login, je copie le token dans la réponse, et le colle dans le champ Token de l’onglet Authorization.

Je saisis ensuite les champs requis par le schéma en mettant les intitulés des champs en clé et donne les valeurs de mon choix. Je copie également le userId depuis ma requête Login, et la colle comme valeur de mon champ userId. j’envoie ensuite la requête en tapant sur le bouton “Send” :

Image du test de la fonction createBeer avec Postman
Test de la fonction createBeer() avec Postman

Nous recevons bien une réponse de succès avec un code de statut 201, et le message de succès indiqué dans notre fonction. 

Vérification de l’enregistrement en base de données

Nous pouvons également vérifier que notre produit a bien été enregistré dans notre base de données MongoDB. Pour cela rendez vous sur notre cluster MongoDB, et dans l’onglet database, allons voir notre collection. 

A noter, qu’effectivement la base de données MongoDB est fractionnée en collections, et que par défaut le nom de cette collection est défini sur le pluriel du nom du modèle. Ici, ce sera donc products.

Je peux voir que mon nouveau produit a bien été enregistré dans ma base de données, et qu’un Id, lui a bien été attribué. 

Image de l'enregistrement d'une nouvelle bière en base de données
Vue de l'enregistrement d'une nouvelle bière en base de données

Récupérer toutes les bières

Une fois l’authentification de l’utilisateur faite, nous le redirigeons sur la page affichant tous les avis publiés sur les bières. Nous devons récupérer la liste de toutes les bières présentes en base de données.

Fonction pour récupérer la liste des bières

Continuons notre CRUD, et implémentons la fonction capable de récupérer la liste des bières depuis la base de données. Dans notre contrôleur beer.js, après ma fonction createBeer(), je crée donc une nouvelle fonction : getAllBeers()

				
					//; Récupérer toutes les bières
exports.getAllBeers = (req, res, next) => {
   Beer.find()
      .then(beers => res.status(200).json(beers))
      .catch(error => res.status(400).json({ error }));
};

				
			

Que faisons nous ci-dessus ? 

  • Nous créons notre fonction que nous appelons getAllBeers()
  • Puis nous utilisons la méthode find() de Mongoose afin de renvoyer un tableau de tous les produits contenus dans notre base de données.

Mettons en place la route correspondante.

Route GET pour l’affichage de la liste des avis de bières

Continuons, et implémentons notre route GET afin qu’elle nous renvoie toutes les bières enregistrées dans la base de données. 

Dans le fichier routes/beers.js, après ma route POST, je code ceci : 

				
					//: Route pour récupérer toutes les bières
router.get('/',auth, beerCtrl.getAllBeers);

				
			

Je garde le même endpoint qui est la racine de ma route déjà déclarée dans mon fichier app.js. Mais ici, nous sommes sur la route utilisant la méthode GET

Noter que je met en attribut le middleware d’authentification, afin que seuls les utilisateurs connectés verront cette liste.

Test de la fonction getAllBeers() et la route GET sur Postman

Pour tester sur Postman, nous devons donc créer une nouvelle requête, que je nomme Get All Beers, par exemple. Nous devons mettre le token dans l’onglet Authorization, et ensuite la requête est donc de type GET sur le endpoint http://localhost:3000/api/beers et nous cliquons ensuite sur “Send” :

Image du Test de la fonction GetAllBeers() dans Postman
Test de la fonction GetAllBeers() dans Postman

Nous recevons bien un statut 200 de succès et la liste des bières enregistrées dans la base de données. 

Cette fois, vous pouvez aussi tester sur le frontend si vous le souhaitez. Si vous avez enregistré plusieurs bières à l’aide de Postman et de la fonction createBeer(), lorsque vous vous connecterez en tant qu’utilisateur, la liste des bières enregistrées s’affichera. Sans image bien entendu puisque nous n’avons pas coder cette fonctionnalité pour le moment. 

Poursuivons notre CRUD pour nos bières. 

Récupérer une bière spécifique

Lorsque nous sommes sur la page affichant tous les avis de bières, nous devons faire en sorte que lorsque l’utilisateur clique sur une bière, cela le renvoie vers cet avis de bière spécifique.

Pour récupérer cet avis de bière spécifique, nous utiliserons son id, qui je vous le rappelle est unique. Nous passerons ensuite cet id comme paramètre de notre route

Mais faisons les choses dans l’ordre, et rendons nous dans notre contrôleur, pour coder la fonction qui nous permettra de récupérer une bière spécifique. 

Dans controllers/user.js je code : 

				
					//; Récupérer une seule bière
exports.getOneBeer = (req, res, next) => {
   Beer.findOne({ _id: req.params.id })
      .then(beer => res.status(200).json(beer))
      .catch(error => res.status(404).json({ error }));
};

				
			

Nous codons ci-dessus : 

  • Nous utilisons la méthode findOne() de notre modèle Mongoose pour trouver le produit unique ayant le même _id que le paramètre de la requête (id).
  • Ce produit unique est retourné dans une promesse, avec en cas de succès un code de status 200. En cas d’erreur, le bloc catch(), nous renvoie un code 404 avec l’erreur générée.

Route GET + paramètre :id pour l’affichage d’ un avis de bière spécifique

Continuons, et implémentons notre route GET afin qu’elle nous renvoie la bière spécifique enregistrée dans la base de données. 

Dans le fichier routes/beers.js, je code donc ceci : 

				
					//: Route pour récupérer une seule bière
router.get('/:id', auth, beerCtrl.getOneBeer);

				
			

Nous utilisons donc la méthode GET, pour répondre aux demandes GET de cet endpoint, qui sera la racine de /api/beers et nous plaçons ensuite deux points : en face du segment dynamique de la route pour la rendre accessible en tant que paramètre.

J’ajoute ensuite le middleware d’authentification en premier attribut, et ensuite mon contrôleur faisant appelle à la fonction getOneBeer()

Test de la fonction getOneBeer(), et de la route GET :id sur Postman.

Je ne vais pas répéter à chaque test, mais vous en avez maintenant l’habitude, il faudra pour les routes des bières, mettre votre token dans l’onglet Authorization, comme nous l’avons vu dans notre test de la fonction createBeer()

Ensuite dans ma nouvelle requête de type GET, je dois saisir le endpoint suivant : http://localhost:3000/api/beers/:id , mais bien entendu, nous devons remplacer le paramètre :id par l’id de la bière que nous voulons afficher. Je peux copier cette id depuis la requête affichant la liste de mes bières (Get All Beers), ou depuis la base de données. Ensuite je clique sur “Send” pour envoyer la requête : 

Image du test de la fonction GetOneBeer avec Postman
Test de la fonction GetOneBeer() avec Postman

Nous pouvons voir que nous avons bien reçu la bière dont l’id a été renseigné à la suite de notre endpoint de notre requête GET

Vous pouvez également tester notre route et notre fonction dans l’application frontend.

Maintenant si vous cliquez sur une bière de la liste, cela vous affichera bien la bière sur laquelle vous avez cliqué. 

On peut aussi remarquer sur le frontend, que si l’utilisateur connecté est le créateur de la bière, les boutons “MODIFY” et  “DELETE” sont présents. Par contre, si l’utilisateur connecté n’est pas le créateur, ces boutons ne sont pas affichés. Normal puisque nous voulons que seul le créateur d’une bière puisse modifier ou supprimer cette bière. 

Pour que ces boutons fonctionnent, nous devons encore implémenter les fonctions de modifications et de suppressions de nos bières, dans notre CRUD

Alors allons y !

Modifier et mettre à jour un avis de bière existant

Afin que le créateur d’une bière puisse modifier les champs qu’il a renseigné lors de la création de celle-ci, nous devrons là aussi, récupérer cette bière spécifique. 

Créons donc une nouvelle fonction au sein de notre contrôleur.

Dans controllers/user.js je code :

				
					//; Modifier et mettre à jour une bière
exports.modifyBeer = (req, res, next) => {
   Beer.updateOne({ _id: req.params.id }, { ...req.body, _id: req.params.id })
      .then(() => res.status(200).json({ message: 'Produit modifiè avec succès !'}))
      .catch(error => res.status(400).json({ error }));
};

				
			

Ci-dessus, nous exploitons la méthode updateOne() dans notre modèle Mongoose.

Cette méthode nous permet de mettre à jour l’objet que nous passons en 1er argument, et nous utilisons le paramètre id passé dans la requête, et nous le remplaçons par le produit passé comme second argument.

Encore une fois, ici nous ne codons rien concernant l’image, mais ne vous inquiétez pas, nous reviendrons modifier cette fonction, lors de l’implémentation de cette fonctionnalité. 

Route PUT + paramètre :id pour modifier un avis de bière

Nous avons vu que la méthode POST est utilisée pour créer, ajouter , que la méthode GET est utilisée pour récupérer, afficher , et bien pour modifier et mettre à jour, nous devons utiliser la méthode PUT

Et comme ici, nous voulons pouvoir modifier et mettre à jour une bière spécifique, nous utiliserons notre paramètre de route :id

Nous allons donc, après la route pour récupérer un seul produit,  coder notre route PUT ainsi :

				
					//: Route pour modifier et mettre à jour une bière
router.put('/:id', auth, beerCtrl.modifyBeer);

				
			

Test avec Postman

Comme d’habitude, je créais une nouvelle requête dans Postman que je nomme Update One Beer. Cette requête est de type PUT, le token est mis, et le endpoint est donc : 

http://localhost:3000/api/beers/:id ou là encore notre paramètre :id doit être remplacé par un _id existant. Je vais donc cibler mon deuxième produit afin de lui apporter des modifications. Le endpoint de mon côté est donc celui-ci : 

http://localhost:3000/api/beers/639df76a307377ecd4faa236

Je vais ensuite réécrire les paires clé/valeurs que je veux modifier. Par exemple, je veux modifier le nom (name) par bière n°3 la description par description de  la bière n°3.

Vous pouvez, bien entendu, essayer de changer d’autres valeurs de votre côté. Une fois les modifications apportées, je vais cliquer sur “Send” : 

Image de test de la fonction modifyBeer() avec Postman
Test de la fonction modifyBeer() avec Postman

Je reçois bien une réponse de succès avec le statut 200, et le message “Produit modifié avec succès“. 

Comme pour la création, nous ne pouvons pas tester sur notre application frontend car celle-ci attend des champs comme l’image que nous n’avons pas encore implémentée. 

Vérification des modifications en base de données.

Afin de vérifier que mes modifications ont bien été faites, j’ai 2 choix possibles. Soit je fais une requête pour récupérer la bière spécifique sur Postman, comme nous l’avons vu précédemment, ou alors je me rends sur la base de données MongoDB, pour voir les informations contenues dans ma deuxième bière, qui est celle que je viens de modifier.

Après actualisation de ma base de données, je peux voir que ma bière à bien été modifiée, et que les champs auxquels je n’ai pas apporté de modification demeurent comme auparavant.

Image de l'enregistrement des modifications

Le nom et la description de mon avis de bière ont bien été modifiés. 

Parfait, finissons notre CRUD avec la possibilité de supprimer une bière. 

Suppression d’un avis de bière

Comme pour nos fonctions et nos routes précédentes, la suppression ciblera une bière spécifique, et nous utiliserons donc là aussi son id. 

Créons donc une nouvelle fonction au sein de notre contrôleur.

Dans controllers/user.js je code :

				
					//; Suppression d'un bière
exports.deleteBeer = (req, res, next) => {
   Beer.deleteOne({ _id: req.params.id })
      .then(() => res.status(200).json({ message: 'Produit supprimé avec succès !'}))
      .catch(error => res.status(400).json({ error }));
};

				
			

Ci-dessus : 

  • Nous utilisons la fonction deleteOne() de Mongoose à laquelle nous passons en paramètre l’id de la bière ciblée dans l’id de la requête.
  • Nous envoyons ensuite dans une promesse soit une réponse de succès, soit une erreur. 

Route DELETE + paramètre :id

La méthode que nous devons utiliser dans nos routes pour supprimer une bière est la méthode DELETE. Et comme nous voulons supprimer une bière spécifique, nous utiliserons là encore le paramètre :id

Après notre route PUT, codons donc notre route DELETE ainsi : 

				
					//:  Route pour supprimer une bière
router.delete('/:id', auth, beerCtrl.deleteBeer);

				
			

Nous mettons bien entendu en premier attribut, après le endpoint, notre middleware d’authentification, puis nous passons en second paramètre notre contrôleur qui utilisera la fonction deleteBeer()

Test avec le frontend de l’application

Nous pouvons tester la suppression d’une bière depuis le frontend de l’application. 

Comme nous l’avons vu, seul le créateur de  la bière aura accès à un bouton pour modifier ou supprimer la bière publiée. 

N’hésitez donc pas à créer des bières depuis Postman avec la requête Create Beer, en utilisant plusieurs utilisateurs différents, et donc pour chacun d’entre eux, vous modifierez le token d’authentification et la valeur du champ userId, que vous récupérez dans la requête Login.. 

Sachez également que la fonctionnalité pour afficher les boutons de modifications ou de suppression en fonction que l’utilisateur est ou non le créateur, est gérée et codée depuis l’application frontend.

Je me connecte donc avec un de mes utilisateurs qui a publié un avis de bière et me rend ensuite sur la bière que je veux supprimer. Je clique sur le bouton DELETE

Image de la présence du bouton DELETE pour createur
Présence du bouton DELETE pour le créateur de la bière uniquement

Après avoir cliquer sur Delete, je suis renvoyé sur la liste de bières, et la bière brune 2 que j’ai supprimée, n’est plus présente dans ma liste. elle a donc bien été supprimée. 

Image de la liste des bières sans la bière supprimée précédemment
Liste des bières sans la bière supprimée précédemment

Vous pouvez aussi le vérifier en base de données, bien entendu.

CRUD Produits complet

Félicitations ! Nous venons d’implémenter le CRUD complet pour nos bières.

Le CRUD implémenté, permettra ainsi un parcours utilisateur complet. 

Si nous résumons où nous en sommes, nous pouvons dire que notre application a bien pris forme.

  • Nous avons vu comment utiliser Mongoose pour créer un modèle de données afin de faciliter les opérations de la base de données. 
  • Nous avons implémenté le cryptage de mot de passe sécurisé afin de stocker les mots de passe de nos utilisateurs en toute sécurité.
  • Nous avons créé et envoyé des tokens au frontend pour authentifier les requêtes.
  • Et enfin, nous avons ajouté le middleware d’authentification pour sécuriser les routes de notre api.
  • Nous avons également implémenté dans notre application Express, les routes CRUD qui exploitent notre modèle de données pour nos avis de bières. 
  • Nous avons rendu notre application entièrement dynamique. 

Mais il reste encore du travail, et dans le prochain chapitre, nous allons voir comment gérer les images de nos bières, et de manière globale, comment gérer les fichiers dans notre api.

Vous me suivez ?

Chapitre 6 - Ajout de la gestion des fichiers utilisateur sur l’application

Notre application fonctionne correctement et est déjà fonctionnelle. Par contre jusqu’à maintenant, nous avons laissé volontairement la possibilité d’ajouter une image à nos bières. Il est temps de s’en occuper. 

C’est pourquoi nous devons implémenter la possibilité de télécharger des fichiers pour que nos utilisateurs puissent télécharger les images de leur produit (leur bière), directement.

Pour gérer ces fichiers entrant dans les requêtes HTTP, nous allons avoir besoin d’un nouveau package qui se nomme multer

Nous allons donc installer ce package, en tapant dans le terminal depuis notre dossier backend :

				
					 npm install multer
				
			

Configuration du middleware de gestion de fichiers

Après l’installation du package, organisons nous. Dans notre dossier backend, nous allons créer un dossier que nous nommons images

Puis dans notre dossier middleware, créons un nouveau fichier multer-config.js.

Codons ce middleware ainsi : 

				
					const multer = require('multer');
 
const MIME_TYPES = {
    'image/jpg': 'jpg',
    'image/jpeg': 'jpg',
    'image/png': 'png',
};
 
const storage = multer.diskStorage({
    destination: (req, file, callback) => {
        callback(null, 'images');
    },
    filename: (req, file, callback) => {
        const name = file.originalname.split('.')[0].split(' ').join('_');
        const extension = MIME_TYPES[file.mimetype];
        callback(null, name + '_' + Date.now() + '.' + extension);
    }
});
 
module.exports = multer({ storage: storage }).single('image');
				
			

Que faisons nous dans ce middleware

  • Nous importons notre package multer.
  • Nous créons ce que nous appelons, un dictionnaire qui contient le type de fichiers que notre application acceptera lors des téléchargements.
  • Nous créons une constante “storage”, à passer à multer comme configuration, qui contient la logique nécessaire pour indiquer à multer où enregistrer les fichiers entrants : 
    • La fonction destination indique à multer d’enregistrer les fichiers dans le dossier images.
    • La fonction filename indique à multer d’utiliser le nom d’origine, puis de remplacer les espaces par des underscores, et d’ajouter ensuite un timestamp Date.now() comme nom de fichier. Elle utilise ensuite la constante dictionnaire de type MIME pour résoudre l’extension de fichier appropriée.
  • Nous exportons ensuite l’élément multer entièrement configuré, lui passons notre constante storage et lui indiquons que nous gérerons uniquement les téléchargement de fichiers nommés ‘image‘. ATTENTION, ici cela signifie que le champ de type “file” aura la clé, (la key) ‘image‘. A ne pas confondre avec le type de fichiers qui est défini dans notre dictionnaire. Nous aurions pû d’ailleurs indiquer un autre mot, et dans ce cas la clé du champ devrait être nommée comme ce mot. 

Avant de pouvoir appliquer notre middleware à nos routes beers, nous devons les modifier, car la structure des données entrantes n’est pas la même avec des fichiers et des données JSON. 

Modification des routes et des contrôleurs pour prendre en compte les fichiers

Pour que notre middleware de téléchargement de fichiers fonctionne sur nos routes, nous devons les modifier, car le format d’une requête contenant un fichier du frontend est différent. 

Modification de la route POST

Tout d’abord nous devons ajouter et importer notre middleware multer-config.js à notre routeur routes/beers.js :

				
					//; Importation du middleware multer-config
const multer = require('../middleware/multer-config');


				
			

Nous ajoutons ensuite multer comme attribut de notre route POST

				
					//: Route pour créer ou ajouter une bière
router.post('/',auth, multer, beerCtrl.createBeer );

				
			

ATTENTION, l’ordre des middleware est important. Si, par exemple, nous plaçons multer avant le middleware auth, cela signifie que nous acceptons les images des requêtes non authentifiées, et que le téléchargement d’images sera toujours accepté. Nous ne voulons pas cela, et c’est pourquoi, nous veillons à placer le middleware multer après le middleware auth

Modification de la fonction createBeer() dans le contrôleur

Pour gérer correctement la nouvelle requête entrante, nous devons mettre à jour notre contrôleur controllers/beer.js, et notre fonction createBeer() :

				
					//; Créer (ajouter) une bière
exports.createBeer = (req, res, next) => {
   const beerObject = JSON.parse(req.body.beer);
   delete beerObject._id;
    const beer = new Beer({
       ...beerObject,
       imageUrl: `${req.protocol}://${req.get('host')}/images/${req.file.filename}`,
       likes: 0,
       dislikes: 0
    });
    beer.save()
       .then(() => res.status(201).json({ message: 'Objet enregistré avec succès !' }))
       .catch(error => res.status(400).json({ error }));
 };

				
			

Que fait ce code : 

  • Pour ajouter un fichier à la requête, le frontend doit envoyer les données de la requête sous la forme form-data, et non sous forme de JSON. Le corps de la requête contient une chaîne beer, qui est simplement un objet Beer converti en chaîne. Nous devons donc l’analyser à l’aide de JSON.parse() pour obtenir un objet utilisable. 
  • Nous devons également résoudre l’URL complète de notre image, car req.file.filename ne contient que le segment filename. Nous utilisons req.protocol pour obtenir le premier segment (dans notre cas ‘http). Nous ajoutons ‘://’, puis utilisons req.get(‘host’) pour résoudre l’hôte du serveur (ici, ‘localhost:3000‘). Nous ajoutons finalement ‘/images/’ et le nom de fichier “filename” pour compléter l’URL.

 

ATTENTION, ici nous utilisons JSON.parse() pour pouvoir ajouter une bière depuis notre application frontend. Si nous essayons d’ajouter une bière depuis Postman, nous aurons une erreur. Pour tester sur Postman, il faudrait enlever JSON.parse(), et laisser uniquement 

				
					const beerObject =  req.body
				
			

Mais comme nous avons notre application frontend, profitons en et utilisons là.

Test depuis l’application frontend

Depuis le frontend de l’application, je me connecte avec un utilisateur valide, et je clique ensuite sur le bouton ADD BEER présent dans le header.

Je renseigne ensuite tous les champs demandés, et télécharge une image pour ma bière.

Vous remarquerez que tant que tous les champs ne sont pas remplis, le bouton SUBMIT n’est pas actif. 

Après avoir renseigné donc tous les champs, je clique sur ce bouton, et je suis renvoyé sur la liste des bières publiées.

Ma bière apparait bien, mais malgré tout il y a une erreur sur l’affichage de l’image.

Et si nous regardons dans les outils de navigateur et plus particulièrement dans la console, nous avons ce message : 

Failed to load resource: the server responded with a status of 404 (Not Found)

Pourquoi ? 

En fait, nous effectuons une demande vers http://localhost:300/images/nomdelimage.jpg.

Pour l’instant, notre application s’exécute sur localhost:3000, mais nous ne lui avons pas indiqué comment répondre aux requêtes transmises à la route de notre image. Elle nous renverra donc une erreur 404.

Pour remédier à cela, nous devons indiquer dans notre fichier app.js, comment traiter les requêtes vers la route /image, en rendant notre dossier images statique.

Accès au chemin du serveur

Pour rendre notre dossier images statique, nous devons faire une nouvelle importation dans app.js pour accéder au “path“(chemin en français) de notre serveur : 

				
					const path = require('path');
				
			

Ici, nous n’avons pas besoin d’installer un package, car path est accessible nativement dans node.js

Nous devons ensuite ajouter le gestionnaire de routage suivant, au dessus de nos routes déjà présentes dans app.js

				
					app.use('/images', express.static(path.join(__dirname,'images')));

				
			

Nous indiquons ainsi à Express de gérer la ressource images de manière statique (un sous-répertoire de notre répertoire de base, __dirname) à chaque fois qu’elle reçoit une requête vers la route /images.

Désormais, tout doit fonctionner correctement. 

Si nous actualisons l’application, que nous nous connectons de nouveau, nous pouvons voir que l’application affiche bien les images qui ont été téléchargées lors de la création de nos bières.

Image de l'Affichage des images sur le frontend de l'application
Affichage des images de bières sur le frontend de l'application

Parfait ! Continuons et modifions les autres routes et fonctions.

Modification de la route PUT

La modification de notre route PUT est plus complexe. En effet, nous devons prendre en compte 2 possibilités, lors de la modification d’une bière. La première est que l’utilisateur modifie et met à jour l’image, et la seconde est qu’il ne modifie pas l’image. 

Autrement dit, dans le premier cas, nous recevons un élément form-data et le fichier, et dans le second, nous recevons uniquement des données JSON

Faisons les choses dans l’ordre, et commençons par ajouter notre middleware multer à notre route PUT.

				
					//: Route pour modifier et mettre à jour une bière
router.put('/:id', auth, multer, beerCtrl.modifyBeer);
				
			

A présent, nous devons modifier notre fonction modifyBeer() dans notre contrôleur, pour voir si nous avons reçu ou non un nouveau fichier, et répondre en conséquence : 

Modification de la fonction modifyBeer() dans le contrôleur

Pour gérer correctement la nouvelle requête entrante, avec les 2 possibilités évoquées juste avant, nous devons mettre à jour notre contrôleur controllers/beer.js, et notre fonction modifyBeer() :

				
					//; Modifier et mettre à jour une bière
exports.modifyBeer = (req, res, next) => {
   const beerObject = req.file ? {
      ...JSON.parse(req.body.beer),
      imageUrl: `${req.protocol}://${req.get('host')}/images/${req.file.filename}`
   } : { ...req.body };
   
    Beer.updateOne({ _id: req.params.id }, { ...beerObject, _id: req.params.id })
       .then(() => res.status(200).json({ message: 'Produit modifié avec succès !'}))
       .catch(error => res.status(400).json({ error }));

				
			

Dans cette version modifiée de la fonction :

  • On crée un objet beerObject qui regarde si req.file existe ou non. 
    • Noter qu’ici nous utilisons ce que nous appelons une ternaire plutôt qu’une condition if/else avec le  “ ? “ signifiant “si” et le “ : ” signifiant “sinon”. C’est une façon de réduire le code écrit.
  • S’il existe, on traite la nouvelle image. 
  • S’il n’existe pas, on traite simplement l’objet entrant. 
  • On crée ensuite une instance Beer à partir de beerObject, puis on effectue la modification avec la méthode updateOne()

Test sur l’application frontend

Testons la modification d’une bière, en nous connectant avec un utilisateur ayant publié une bière, et modifions quelques champs et également l’image. 

Image de la modification d'une bière sur l'application frontend
Modification d'une bière sur l'application frontend

Les modifications apportées sont bien prises en charge et cela est fonctionnel

Félicitations ! Notre application gère maintenant la modification des bières avec des images. 

Laissons cela comme ça pour le moment, car je pense que nous pouvons encore améliorer le processus. Mais une chose après l’autre… 

Continuons à modifier nos routes et finissons par la route DELETE

Modification de la route DELETE

Nous allons avoir besoin pour cela du package fs de Node.js. Comme il est natif, nous avons besoin uniquement de l’importer dans notre contrôleur controllers/beer.js :

				
					const fs = require('fs');
				
			

Pour info, fs signifie “file system”, ou système de fichiers en français. Il nous donne accès aux fonctions permettant de modifier le système de fichiers, y compris la suppression de fichiers.

Modifions maintenant notre fonction deleteBeer() :

				
					//;  Supprimer une bière
exports.deleteBeer = (req, res, next) => {
   Beer.findOne({ _id: req.params.id})
      .then(beer => {
         const filename = beer.imageUrl.split('/images/')[1];
         fs.unlink(`images/${filename}`, () => {
            Beer.deleteOne({ _id: req.params.id })
            .then(() => res.status(200).json({ message: 'Votre bière a été supprimée avec succès !'}))
            .catch(error => res.status(400).json({ error }));
         });
      })
      .catch(error => res.status(500).json({ error }));
 };

				
			

Dans cette fonction modifiée : 

  • Nous utilisons l’ID que nous recevons comme paramètre pour accéder à la bière correspondante dans la base de données. 
  • Nous utilisons le fait de savoir que notre URL d’image contient un segment /images/ pour séparer le nom du fichier.
  • Nous utilisons ensuite la fonction unlink() du package fs pour supprimer ce fichier, en lui passant le fichier à supprimer et le callback à exécuter une fois ce fichier supprimé. 
  • Dans le callback, nous implémentons la logique d’origine, en supprimant la Beer avec l’id spécifié de la base de données.

Test sur l’application frontend

Nous pouvons tester la suppression d’une bière depuis notre application frontend. Comme expliqué précédemment, nous devons pour cela, que l’utilisateur connecté soit le créateur de la bière. Après avoir sélectionné une des bières créées par l’utilisateur, je clique simplement sur le bouton DELETE

Image de la modification d'une bière sur l'application frontend

La bière que je viens de supprimer n’apparaît plus dans la liste, et n’est plus présente non plus en base de données.

Image de l'absence de la bière supprimée de la liste des bières
Absence de la bière supprimée de la liste des bières

Modification de notre modèle Beer

Nous l’avons vu, lorsque nous créons une bière, il faut que tous les champs du formulaire de création soient renseignés pour que le bouton “SUBMIT” soit actif. 

Il paraît donc naturel, d’apporter maintenant une petite modification à notre modèle Beer.js, et de simplement passer la valeur de “required” sur le champ imageUrl, de false à true

				
					const mongoose = require('mongoose');
 
const beerSchema = mongoose.Schema({
    userId: { type: String, required: true },
    name: { type: String, required: true },
    manufacturer: { type: String, required: true },
    description: { type: String, required: true },
    mainIngredient: { type: String, required: true },
    imageUrl: { type: String, required: true },
    degree : { type: Number, required: true },
    likes: { type: Number, required: true },
    dislikes: { type: Number, required: true },
    usersLiked: { type: Array, required: true },
    usersDisliked: { type: Array, required: true },
});
 
module.exports = mongoose.model('Beer', beerSchema);
				
			

Épilogue

Vous pouvez à ce stade, tester de nouveau toutes les fonctionnalités de ce CRUD. Je vous laisse faire, car je pense que maintenant vous savez parfaitement faire cela.

Notre api peut désormais gérer correctement toutes les opérations CRUD contenant des fichiers. 

Nous l’avons vu, nous avons implémenté dans notre application et dans notre modèle Beer.js des champs concernant des likes et des dislikes.

Il nous reste donc à ajouter ce système de likes & dislikes

Alors vous me suivez toujours ? Allons y… 

Chapitre 7 - Système de likes et de dislikes

Pour implémenter ce système de likes et de dislikes, nous avons anticipé cela en ajoutant à notre modèle les champslikes”, “dislikes”, “usersLiked” et “usersDisliked”. 

Les 2 premiers champs sont de type “Number” et les 2 derniers sont de type “Array”. 

En effet, afin qu’un utilisateur ne puisse pas “liker” ou “disliker” une bière plusieurs fois de suite,, nous devons enregistrer l’id de cet utilisateur. Et nous utiliserons donc ces arrays ou ces tableaux à cet effet. 

Mais nous devons aller encore plus loin, car si l’utilisateur ne pourra “liker” ou “disliker” une bière qu’une seule fois, il pourra en revanche changer d’avis et “disliker” une bière qu’il avait auparavant “liker”, et vis et versa. 

Mais comme toujours, mieux vaut pratiquer que de longs discours.
Rendons nous donc dans notre contrôleur controllers/beer.js et codons cette fonction : 

				
					// ! Système de Like et Dislike
 
exports.likeBeer = (req, res, next) => {
   const userId = req.body.userId;
   const like = req.body.like;
   const beerId = req.params.id;
   Beer.findOne({ _id: beerId })
      .then(beer => {
         // nouvelles valeurs à Modifier
         const newValues = {
            usersLiked: beer.usersLiked,
            usersDisliked: beer.usersDisliked,
            likes: 0,
            dislikes: 0
         }
         // Suivant les cas :
         switch (like) {
            // Lorsque bière est "liké"
            case 1:
               newValues.usersLiked.push(userId);
               break;
            // Lorsque bière "disliké"
            case -1:
               newValues.usersDisliked.push(userId);
               break;
            // lorsque pas d'avis ou annulation du like ou dislike
            case 0:
               if(newValues.usersLiked.includes(userId)) {
                  // Si annulation du like
                  const index = newValues.usersLiked.indexOf(userId);
                  newValues.usersLiked.splice(index, 1);
               } else {
                  // Si annulation du Dislike
                  const index = newValues.usersDisliked.indexOf(userId);
                  newValues.usersDisliked.splice(index, 1);
               }
               break;
         };
         // Calcul du nombre de likes et de dislikes
         newValues.likes = newValues.usersLiked.length;
         newValues.dislikes = newValues.usersDisliked.length;
         // Mise à jour des nouvelles valeurs de la bière
         Beer.updateOne({ _id: beerId }, newValues )
            .then(() => res.status(200).json({ message: 'Note attribuée avec succès à cette bière !' }))
            .catch(error => res.status(500).json({ error }))
      })
      .catch(error => res.status(500).json({ error }));
};

				
			

Expliquons ce code : 

  • On commence par récupérer le userId qui est l’id de l’utilisateur, puis la valeur du like dans la requête, et enfin l’id de la bière depuis le paramètre de la requête.
  • On utilise ensuite la méthode findOne() de Mongoose pour récupérer la bière selon son id qui doit être identique à celui passé en paramètre de la requête.  
  • Cette fonction renverra une promesse dans le bloc then/catch 
    • Dans le bloc then, on crée une constante qui récupère les valeurs de notre système qui seront à modifier.
    • Nous utilisons ensuite l’instruction “switch” : 
      • L’instruction switch évalue une expression et, selon le résultat obtenu et le cas associé, exécute les instructions correspondantes.
    • Nous définissons 3 cas possibles, ayant les valeurs 1 si “likée”, -1 si “dislikée” et 0 si annulation du like ou du dislike, ou si pas de likes ou dislikes attribués.
      • Dans ces différents cas, nous utilisons les méthodes push() pour enregistrer l’userId dans les tableaux (arrays) correspondants.
      • La méthode indexOf() permet de renvoyer l’userId dans le tableau correspondant .
      • La méthode splice() pour retirer 1 à l’index de nos tableaux correspondants.
      • Noter bien que nous mettons un break après chaque cas, pour que notre switch fonctionne correctement.
    • Ensuite, nous faisons le calcul du nombre de likes et de dislikes, en fonction de la longueur “length” de chacun des 2 tableaux.
    • Enfin nous mettons à jour les nouvelles valeurs avec la méthode updateOne() de Mongoose, qui renvoie soit à un succès, soit à une erreur. 
  • Le bloc catch, si la fonction échoue, nous renverra une erreur avec le statut 500

Notre fonction est maintenant créée et nous devons mettre en place la route POST pour pouvoir “liker” et “disliker”.

Route POST pour le système de likes et dislikes

Comme nous l’avons codé dans notre fonction, nous allons envoyer des données et nous devons donc pour cela utiliser une route de type POST

Nous utiliserons en paramètre l’id de la bière, pour cibler la bière concernée, et utiliserons également notre middleware d’authentification.

Dans notre routeur routes/beers.js, codons ceci : 

				
					//: Route pour liker/ disliker une bière
router.post('/:id/like', auth, beerCtrl.likeBeer);

				
			

Nous utilisons le endpoint avec /like à la fin, tel que défini dans les spécifications techniques et fonctionnelles du projet.

Test du système de likes et dislikes sur l’application frontend

Maintenant nous pouvons utiliser notre système sur l’application frontend, et nous voyons que lorsqu’un utilisateur clique sur l’icône de like  ou de dislike, le nombre est incrémenté de 1. Si l’utilisateur veut changer son avis, il clique d’abord sur l’icône sur laquelle il a cliquer auparavant pour enlever son choix, et il peut ensuite refaire un autre choix. Ceci je vous le rappelle pour ne pas pouvoir “voter” plusieurs fois pour la même bière. 

Image de système de like et de dislikes
Système de like et de dislikes

Notre système de likes et de dislikes est maintenant fonctionnel, ainsi que l’ensemble des fonctionnalités de notre api

Préambule à la conclusion

Je viens de le dire, notre API est fonctionnelle mais nous verrons en bonus de cette série de tutoriels que nous pouvons y apporter des améliorations, et pour la sécurité de l’application, nous nous devons de le faire. 

Que de chemin parcouru dans ce tutoriel !

Félicitations pour la création de cette api avec Node.js, Express et MongoDB.

Vous souhaitez poursuivre en allant un peu plus loin, alors rendez vous dans le bonus de cette série, après la conclusion qui suit. 

Conclusion Création d’une API avec Node.js, Express et MongoDB

Nous voici arrivés à la fin de cette série de tutoriels consacrée à la création d’une api avec Node.js, Express et MongoDB

Ne vous inquiétez pas, comme toujours sur Activateur Web, vous avez droit à un bonus

Mais avant cela, je veux simplement que vous mesuriez la connaissance acquise à la fin de ce tutoriel : 

  • Vous savez maintenant créer un serveur Node, et l’utiliser pour servir une application Express.
  • Vous avez connecté votre application à une base de données MongoDB, et à l’aide du module de Node.js, Mongoose, vous avez créé une API RESTful permettant les opérations CRUD.
  • Vous avez appris à structurer votre code, et à le rendre plus robuste, lisible et maintenable facilement.
  • Vous avez implémenté une authentification sécurisée à base du token JWT.
  • Vous avez implémenté la gestion du téléchargement de fichiers, permettant ainsi aux utilisateurs d’ajouter et de supprimer des images.
  • Vous avez également appris à utiliser l’outil Postman, vous permettant de tester vos contrôleurs, vos routes et toutes les fonctionnalités créées, au fur et à mesure du développement de votre api.

Félicitations ! Vous pouvez être fier de vous. 

Vous avez maintenant une application à laquelle vous pouvez vous connecter pour utiliser ces fonctionnalités sur n’importe quel frontend

Bien entendu, nous avons, dans cette série, développé uniquement un schéma de produits et un schéma d’utilisateurs, mais libre à vous d’ajouter d’autres modèles, contrôleurs, routes, selon le besoin de votre application.  

J’espère que vous aurez pris plaisir à suivre cette série, tout autant que j’ai pris plaisir à la construire. 

Comme d’habitude, si vous souhaitez soutenir mon travail, et m’encouragez à continuer de produire du contenu, merci de prendre quelques secondes pour vous inscrire sur Activateur Web, à vous abonnez à ma page facebook, à ma chaîne Youtube, à liker, partager, et à me laisser vos commentaires. Créer cette série m’a pris plusieurs longs jours… alors merci à ceux qui prendront quelques secondes pour me soutenir. 

Surtout prenez soin de vous, et surtout restez curieux ! 

Ah, oui je vous l’ai dit, sur Activateur web, il y a souvent un BONUS en plus..

Alors, je vous propose dans ce bonus, d’aller un peu plus loin dans le développement de notre application, et de lui apporter quelques améliorations. 

Vous êtes toujours là ? Ok, alors suivez moi et voyons les améliorations que je vous propose de mettre en place.

BONUS Allons plus loin en améliorant notre API

Amélioration 1 : Modification et mise à jour de l’image d’une bière

Je ne sais pas si de votre côté, vous avez fait attention à cela, mais lorsque nous modifions un bière, en remplaçant une image, et bien l’image remplacée reste dans notre dossier images. 

Refaisons un test pour s’en assurer, et regardons aussi dans  la base de données. J’ai actuellement ma bière N°7 qui dispose de l’imagebiere-du-sorcier_1671…..jpg” :

Dans la base de données : 

Image de l'image enregistrée en base de données
Image enregistrée en base de données

Dans mon dossier images, cette image est bien présente : 

Image de l'image présente dans le dossier "images" de l'api
Image présente dans le dossier "images" de l'api

Maintenant dans l’application frontend, je vais remplacer mon image, en remplaçant l’image existantebiere-du-sorcier_1671…..jpg” , par une image nommée “bière brune”, par exemple. Pour cela en étant connecté avec l’utilisateur qui a créé la bière, je clique sur la bière, puis sur le bouton “MODIFY”, et je modifie l’image en cliquant sur le bouton “ADD IMAGE”, et en allant chercher l’image “bière brune” dans mon explorateur : 

Image du remplacement de l'image depuis la modification d'une bière sur le frontend
Remplacement de l'image depuis la modification d'une bière sur le frontend

Après avoir cliquer sur le bouton “SUBMIT”, je vois que mon image a bien été modifiée, et en actualisant la base de données, je vois qu’elle a bien été modifiée également : 

Image de la base de données après remplacement de l'image
Image remplacée et enregistrée dans la base de données

Mais dans mon dossier images, l’image “biere-brune” a bien été ajoutée, mais l’ancienne imagebiere-du-sorcierdemeure

Image de l'image remplacée qui est toujours dans le dossier "images" de l'api
L'image remplacée est toujours dans le dossier "images" de l'api

Ceci n’est pas optimal, n’est ce pas ? L’amélioration que nous devons apporter ici est donc de supprimer l’image lorsque celle-ci est remplacée. Ainsi, nous ne conserverons pas une image dont nous n’avons pas besoin dans notre dossier images

La fonction que nous devons modifier est la fonction modifyBeer(), située dans notre contrôleur beer.js.

Faisons donc ceci, et dans notre fichier controllers/beer.js, modifions cette fonction en faisant ainsi :

				
					//; Modifier et mettre à jour une bière
 exports.modifyBeer = (req, res, next) => {
   if(req.file) {
      Beer.findOne({ _id: req.params.id })
         .then(beer => {
            const filename = beer.imageUrl.split('/images/')[1];
            fs.unlink(`images/${filename}`, () => {
               const beerObject = {
                  ...JSON.parse(req.body.beer),
                  imageUrl: `${req.protocol}://${req.get('host')}/images/${req.file.filename}`
               }
               Beer.updateOne({ _id: req.params.id }, { ...beerObject, _id: req.params.id })
                  .then(() => res.status(200).json({ message: 'La Bière a été modifiée avec succès !'}))
                  .catch(error => res.status(400).json({ error }));
            })
         })
         .catch(error => res.status(500).json({ error }));
   } else {
      const beerObject = { ...req.body };
      Beer.updateOne({ _id: req.params.id }, { ...beerObject, _id: req.params.id })
         .then(() => res.status(200).json({ message: 'Votre bière a été modifiée avec succès !'}))
         .catch(error => res.status(400).json({ error }));
   }
 };

				
			

Qu’avons nous fait ci-dessus ? 

  • Tout d’abord, nous commençons avec une condition if/else , avec dans le bloc if, nous regardons si une requête est faite sur un fichier (req.file).
    • Si c’est le cas nous récupérons la bière selon son id, passé en paramètre de la requête, et dans le bloc then, on commence par supprimer l’ancienne image avec la fonction fs.unlink(), comme nous l’avions déjà vu, dans notre fonction deleteProduct().
    • Ensuite nous ajoutons le corps de la requête et sa nouvelle image.
    • Enfin, nous mettons à jour en base de données avec la fonction updateOne(), et renvoyons une réponse dans un bloc then/catch, suivant le succès ou l’erreur. 
  • Dans le bloc else, qui est donc le cas où nous ne modifions pas l’image, on récupère le corps de la requête, dans sa totalité avec l’opérateur spread ().
    • Enfin, nous mettons à jour en base de données avec la fonction updateOne(), et renvoyons une réponse dans un bloc then/catch, suivant le succès ou l’erreur. 

Test de la fonction modifyBeer() améliorée

Nous pouvons maintenant tester cela, toujours avec notre application frontend

Je vais remplacer cette fois, mon image nommée “biere-brune” par une image nommée “biere-blanche”, par exemple. Pour l’instant, nous l’avons vu l’image “biere-brune” est bien présente dans mon dossier images

Je modifie donc depuis mon frontend :

Image de la modification image après modification de la fonction modifyBeer()
Modification image après modification de la fonction modifyBeer()

Après avoir fait la modification en cliquant sur le bouton “SUBMIT”, mon image “biere-brunea bien été remplacée par celle nommée “biere-blanche”.

Mais maintenant lorsque nous regardons dans le dossier images, l’image nommée “biere-brune” a bien été supprimée : 

Illustration de l'image de la bière supprimée, est bien supprimée du dossier "images"
L'image de la bière supprimée, est bien supprimée du dossier "images"

Parfait ! Notre CRUD est maintenant optimisé, et nous n’aurons plus à stocker des images qui ne servent pas, ou plus. 

Ce qui pour le poids et la performance de notre site et serveur est beaucoup mieux. 

Vous souhaitez encore aller plus loin ? Alors allons y ! 

Amélioration n°2 : Validation de données des champs utilisateurs

La encore, peut être que certains d’entre vous, l’auront déjà remarqué, mais quelque chose d’essentiel est à améliorer dans notre api : la validation de la saisie des données

Je vous explique ce que j’entends par là. 

Revenons à l’inscription d’un utilisateur, par exemple, et par conséquent à nos fonctions signup(), ou login()

Si je vais sur Postman pour créer un nouvel utilisateur, et que dans le champ email ,je ne rentre pas une adresse email valide, mais uniquement des lettres ou des mots, par exemple activateurweb, et bien rien ne m’empêche de créer mon utilisateur.

Noter que ceci n’est pas possible sur le frontend, car celui-ci attends un champ email valide, puisque le formulaire contient le champ de type email, qui impose un email valide.

Essayons cela sur Postman

Image de la fonction signup() avant la validation de données sur Postman
Fonction signup() avant la validation de données sur Postman

L’utilisateur est bien créé, et si je regarde maintenant dans ma base de données :

Image de l'enregistrement fonction signup() en base de données avant validation de données
Enregistrement fonction signup() en base de données avant validation de données

Mon utilisateur a bien été enregistré ! 

Et il en va de même pour le champ du mot de passe. Si je renseigne uniquement la lettre a, par exemple, comme mot de passe, l’inscription est validée. Alors, oui mon mot de passe sera bien haché, mais c’est pas le top, tout de même. 

Je ne sais pas ce que vous en pensez, mais tout ça n’est pas très logique, et nous ne devons pas laisser cela ainsi. Et c’est justement pour résoudre cela que nous devons mettre en place, la validation des données saisies dans les champs. Et nous en profiterons pour valider ces données, que ce soit pour s’inscrire, ou pour se connecter.

Pour mettre en place cette validation, nous aurons besoin d’installer un package se nommant Joi. Ce package permet de valider les données codées en JavaScript

Installons le, en tapant dans notre terminal : 

				
					npm install joi
				
			

Puis nous allons créer un nouveau middleware. Dans notre dossier middleware, créons un nouveau fichier, que nous nommons validate-inputs.js

Dans ce nouveau fichier, la première chose à faire est d’importer le package : 

				
					const Joi = require('joi');

				
			

Ensuite, nous créons nos validations en utilisant la fonction object() de Joi, qui permet de générer un objet de schéma, qui correspond à un type de données que nous lui passerons en paramètres. 

Voici le code pour nos utilisateurs, dans le fichier middleware/validate-inputs.js :

				
					const userSchema = Joi.object({
    email: Joi.string().trim().email().required(),
    password: Joi.string().trim().min(4).max(30).required()
});
exports.user = (req, res, next) => {
    const { error, value } = userSchema.validate(req.body);
    if(error) {
        res.status(422).json({ error: 'email ou mot de passe invalide !' });
    } else {
        next();
    }
};

				
			

Ci-dessus, 

  • Nous passons donc notre modèle userSchema comme objet de schéma Joi, ou nous déterminons les clés que nous voulons pour valider les données du champ email et du champ password.
    • Pour l’email, nous demandons à valider que ce soit une chaîne de caractères (string), qu’il n’y ait pas d’espaces (trim), que ce soit de type email, et que ce champ est requis.
    • Pour le password, les mêmes clés sont définies, sauf qu’au lieu de demander que ce soit de type email, nous demandons que la chaîne de caractères (le mot de passe) fasse au moins 4 caractères, et au maximum 30 caractères.
  • Nous exportons ensuite cette validation de données, en la nommant user, et nous utilisons une condition if/else
    • Si les champs contiennent une erreur, nous envoyons un code 422 avec un message d’erreur.
    • Si les champs sont valides, nous continuons et poursuivons le code de nos fonctions signup() et login()

Il nous reste à importer ce middleware sur nos routes users.js

Dans le fichier routes/users.js, nous importons le middleware : 

				
					const validate = require('../middleware/validate-inputs');

				
			

Puis ajoutons notre middleware en attributs sur nos routes signup et login, en lui ajoutant la fonction que nous avons nommée “user” :

				
					router.post('/signup',validate.user, userCtrl.signup);
 
router.post('/login',validate.user, userCtrl.login);

				
			

Prêt pour le test de validation de données utilisateurs ? 

Test validation des données utilisateur sur Postman

Je crée pour l’occasion, un nouveau dossier sur Postman que je nomme Validations-données-user

Ensuite je créais une nouvelle requête de type POST sur le endpoint http://localhost:3000/api/auth/signup . Et dans un 1er temps, je vais rentrer comme valeur du champ email, activateurweb, par exemple, qui n’est pas une adresse email valide donc, et je renseigne password correctement. Je tape ensuite sur “Send” : 

Image de Test validation de données utilisateurs avec Postman
Test validation de données utilisateurs avec Postman

Comme coder, nous recevons une erreur avec un code de statut 422, et le message “email ou mot de passe invalide !”. 

Nous pouvons essayer la même chose en mettant cette fois une adresse email valide, et un mot de passe mais de seulement 3 caractères : 

Test n°2 de validation de données utilisateurs avec Postman

Je reçois bien là aussi, une erreur de validation des données saisies.

Il nous reste à maintenant saisir les champs correctement avec une adresse email valide, et un mot de passe de 4 caractères minimum, et 30 caractères maximum

Image de Test n°3 de validation de données utilisateurs avec Postman
Test n°3 de validation de données utilisateurs avec Postman

Cette fois, je reçois bien le message codé dans ma fonction signup() avec un code de statut 201, et mon utilisateur est enregistré en base de données. 

En ce qui concerne la fonction login(), je considère que vous savez maintenant travailler avec Postman, et vous laisse donc tester la connexion utilisateur de votre côté.

C’est en pratiquant que nous apprenons le mieux. 

J’ai essayé de mon côté en entrant pour commencer un email invalide, et j’ai bien ma réponse d’erreur, puis j’ai mis un email valide, mais un mot de passe de seulement 3 lettres, et là aussi, j’ai eu un message d’erreur. Et enfin, j’ai renseigné la bonne adresse email et le bon mot de passe (existants en base de données bien entendu), et cette fois j’ai reçu mon id utilisateur et mon token en réponse

Parfait, nous venons d’implémenter la validation de données des champs utilisateurs

Sachez que vous pouvez pousser le bouchon encore plus loin, et par exemple, ajouter d’autres clés de validation. Pour cela vous pouvez retrouver la liste de ces clés et des exemples de mises en œuvre sur la documentation joi

Mais franchement, ce que nous venons d’implémenter est déjà pas mal. 

Et si nous en profitions pour améliorer la validation des données de nos bières, lorsqu’on crée un nouvel avis de bière, ou lorsqu’on le modifie ? 

Je vous propose un challenge. Essayez d’implémenter ces validations de données par vous-même ! Nous le faisons dans le même fichier validate-inputs.js, mais pour le modèle de bières. 

Je vous laisse coder, j’en profite pour boire un café, et je vous donne ensuite ma façon de faire. Happy coding les amis ! 

Amélioration n°3 -Validation de données des champs du modèle bières

Alors vous y êtes arrivé ? Encore une fois, le principal est le résultat, et non la façon de faire. Il existe en effet, un tas de façon de faire. Le principal étant le résultat. 

Avant toute chose, faisons des tests avant la mise en place de nos validations : 

Cette fois, les tests peuvent se faire aussi bien sur Postman que sur l’application frontend.

Lors de la création d’une bière, si, par exemple, dans le champ “name, je saisi “a”, dans le champ description, je saisi “a”, et dans le champ manufacturer, je saisi “222”, et bien rien ne me l’empêche. 

Ma bière est enregistrée avec succès dans la base de données.

Hors, une bière qui se nomme a, ayant comme description a, et comme créateur 222, ne veut absolument rien dire ? Et c’est exactement la même chose lorsqu’on modifie une bière. Essayer par vous même. 

Image du Test de la fonction createBeer() avant la mise en place des validations de données
Test de la fonction createBeer() avant la mise en place des validations de données

Allez, je vous montre ma façon de faire, et comment remédier à cela. Dans mon middleware validate-inputs.js, à la suite de la validation des données des champs utilisateurs, voici ce que j’ai coder : 

				
					// Validation des données lors de l'ajout ou modification d'une bière
 
const beerSchema = Joi.object({
    userId: Joi.string().trim().length(24).required(),
    name: Joi.string().trim().min(2).required(),
    manufacturer: Joi.string().trim().min(2).required(),
    description: Joi.string().trim().min(5).required(),
    mainIngredient: Joi.string().trim().min(3).required(),
    degree: Joi.number().integer().min(1).max(10).required()
});
exports.beer = (req, res, next) => {
    let beer;
    if(req.file) {
        beer = JSON.parse(req.body.beer);
    } else {
        sauce = req.body;
    }
   
    const { error, value } = beerSchema.validate(beer);
    if(error) {
        res.status(422).json({ error: 'Données invalides !' });
    } else {
        next();
    }
};

				
			

Ci dessus, nous disons : 

  • Le champ name doit être une chaîne de caractères, sans espace, de 2 caractères minimum, et est requis.
  • Le champ description doit être une chaîne de caractères, sans espace, de 5 caractères minimum, et est requis.
  • Le champ manufacturer doit être une chaîne de caractères, sans espace, de 2 caractères minimum, et est requis.
  • Le champ mainIngredient doit être une chaîne de caractères, sans espace, de 3 caractères minimum, et est requis.
  • Le champ degree doit être de type Number, et de type integer, c’est à dire un nombre entier, compris entre minimum 1 et maximum 10, et est requis.
  • Le champ userID, doit être une chaîne de caractères, sans espace, et de 24 caractères de longueur. Lorsqu’on regarde sur MongoDB, nous avons bien 24 caractères désignant l’ID de l’utilisateur.Ici, lenght représente la longueur exacte, c’est-à-dire 24. Pas un de plus ou de moins ! Au contraire de min() ou max() par exemple. Ici, nous mettons 2 car c’est le nombre de caractères utilisés en base de données pour les id utilisateurs.
  • Et enfin on exporte et valide notre schéma, en le nommant “beer” et ensuite :
    • Nous créons une première condition if/else ou si nous envoyons une image, alors nous utilisons JSON.parse() pour renvoyer le corps de l’objet beer, sinon nous renvoyons le corps de la requête.
    • Et enfin nous déclarons une autre condition if/else après la validation, et si erreur, on renvoie une erreur, sinon on exécute le code.  

Nous devons comme nous l’avons fait pour les utilisateurs, importer notre middleware sur nos routes beers. Dans notre fichier routes/beers.js , après l’importation de multer :

				
					//; Importation du middleware validate-inputs
const validate = require('../middleware/validate-inputs');
				
			

Puis nous ajoutons notre middleware en attributs de nos routes POST et PUT, pour la création, et la modification d’une bière :

				
					//: Route pour créer ou ajouter une bière
router.post('/',auth, multer, validate.beer, beerCtrl.createBeer );
 
 
//: Route pour modifier et mettre à jour une bière
 router.put('/:id', auth, multer, validate.beer, beerCtrl.modifyBeer);

				
			

Ceci fait, reste à tester de nouveau.

Test validation de données des champs bières

Je commence par refaire le même test que précédemment. Le champ “name”, je saisi “a”, le champ description, je saisi “a”, et le champ manufacturer, je saisi “222” : 

Cette fois, j’ai beau cliquer sur le bouton “SUBMIT”, cela ne m’envoie pas vers la liste des bières, signe que cela n’est pas validé. 

Vous pouvez tester en essayant de modifier les champs petit à petit, jusqu’à mettre toutes les données valides pour créer ou modifier une bière. 

Après validation des champs, ma bière est bien ajouter à la liste des bières. 

De mon côté, les tests ont été faits, et tout fonctionne correctement.

Nous venons d’implémenter la validation des données des champs de bières, et avons donc apporté une amélioration supplémentaire à notre api

Continuons dans la validation de données, et faisons en sorte de valider le champ id des bières, pour être sûr que l’id des bières que nous utilisons en paramètre de nos routes soit valide. 

Puis je vous propose également de mettre une validation sur notre système de likes et dislikes

Comme vous avez maintenant l’habitude de travailler avec notre api, notre frontend et Postman, je vais aller vite et vous donne simplement le code que j’ai écrit : 

Dans mon middleware de validation et donc dans mon fichier middleware/validate-inputs.js :

				
					// Validation de l'id des bières
const idSchema = Joi.string().trim().length(24).required();
exports.id = (req, res, next) => {
    const {error, value} = idSchema.validate(req.params.id);
    if(error) {
        res.status(422).json({ error: 'Id de la bière invalide !' });
    } else {
        next()
    }
};
 
//  Validation des likes/dislikes
const likeSchema = Joi.object({
    userId: Joi.string().trim().length(24).required(),
    like: Joi.valid(-1, 0, 1).required()
});
exports.like = (req, res, next) => {
    const { error, value } = likeSchema.validate(req.body);
    if(error) {
        res.status(422).json({ error: "Données renseignées invalides ! " });
    } else {
        next();
    }
};

				
			

Dans mon routeur , voici le code complet de routes/beers.js :

				
					const express = require('express');
const router = express.Router();
 
//; Importation du middleware d'authentification
const auth = require('../middleware/auth');
//; Importation du middleware multer-config
const multer = require('../middleware/multer-config');
//; Importation du middleware validate-inputs
const validate = require('../middleware/validate-inputs');
 
const beerCtrl = require('../controllers/beer');
 
//: Route pour créer ou ajouter une bière
router.post('/',auth, multer,validate.beer, beerCtrl.createBeer );
//: Route pour liker/ disliker une bière
router.post('/:id/like', auth, validate.id, validate.like, beerCtrl.likeBeer);
//: Route pour récupérer une seule bière
 router.get('/:id', auth, beerCtrl.getOneBeer);
//: Route pour modifier et mettre à jour une bière
 router.put('/:id', auth, multer, validate.beer, beerCtrl.modifyBeer);
//:  Route pour supprimer une bière
 router.delete('/:id', auth, beerCtrl.deleteBeer);
//: Route pour récuperer toutes les bières
 router.get('/', beerCtrl.getAllBeers);
 
module.exports = router;
				
			

Amélioration n°4 - En-têtes HTTP de sécurité

Les en-têtes HTTP, sont une porte d’entrée pour les pirates informatiques.

C’est pourquoi, nous devons sécuriser ces en-têtes.

Notre site doit définir certains en-têtes HTTP liés à la sécurité. Ces en-têtes sont :

  • Strict-Transport-Security  applique des connexions sécurisées (HTTP sur SSL/TLS) au serveur
  • X-Frame-Options  offre   une protection contre le détournement de clic
  • X-XSS-Protection  active le filtre Cross-site scripting (XSS) intégré dans les navigateurs Web les plus récents
  • X-Content-Type-Options  empêche les navigateurs de renifler MIME une réponse loin du type de contenu déclaré
  • Content-Security-Policy  empêche un large éventail d’attaques, y compris les scripts intersites et d’autres injections intersites

Dans Node.js, il est facile de les définir à l’aide du  module Helmet

Nous devons donc commencer par installer le package Helmet. Dans notre terminal, nous allons donc taper :

				
					npm install helmet
				
			

Ensuite, nous allons l’importer dans notre fichier app.js :

				
					const helmet = require('helmet');

				
			

Et enfin, après nos headers déjà configurés, nous codons :

				
					app.use(helmet({
   crossOriginResourcePolicy: false,
}));

				
			

Ci-dessus, je décide de passer un paramètre à helmet car Google Chrome peut produire des problèmes de CORS sur 2 serveurs communiquant entre eux. Ce paramètre passé à Helmet évitera cette erreur. Sinon vous pouvez simplement coder :

				
					app.use(helmet());

				
			

Voilà, ceci est suffisant pour sécuriser les headers.

Bien entendu, helmet peut être configuré différemment, et d’autres fonctionnalités peuvent être apportées. Si vous le souhaitez, vous pouvez consulter la documentation Helmet afin d’en savoir plus. 

Pour ma part, helmet fera le boulot configuré ainsi. 

Amélioration n°5 - Sécuriser les données sensibles de notre API

Les données sensibles utilisées pour construire notre api, doivent également être sécurisées. Lorsque je parle de données sensibles, je pense,  par exemple, aux identifiants de notre base de données, la clé utilisée pour le token, les données de configuration, etc. Bref, en général, toutes les données qui ne doivent pas être à la portée de tout le monde. Ceci doit être fait, avant même de pousser notre api, par exemple, sur le repository GitHub.

En effet, si nous créons un repository GitHub, à l’heure actuelle et que nous poussons (git push) notre api, dessus, et bien, tout ceux qui ont accès à notre repository, pourront récupérer, notre accès à la base de données MongoDB, notre clé de token, etc. Et si comme moi, vous laissez vos repository en public, et bien tout le monde  pourra y accéder. 

Pour éviter cela, et sécuriser ces données, nous aurons là aussi, recours à un package qui est DotEnv

Dotenv est un package, qui va nous permettre de remplacer ces données par des variables de configuration, constituées de paires clé/valeur. Ces paires clé/valeur seront stockées dans un fichier .env, et nous y accéderons, en écrivant process.env. suivi de la clé. 

Mais pour mieux comprendre, et mieux vous expliquez également 😀, mieux vaut pratiquer. 

Nous commençons donc par installer le package dotenv sur notre projet.

Dans le terminal, je tape donc : 

				
					npm install dotenv 

				
			

Ensuite je vais créer un fichier dans mon dossier backend, que je nomme .env .

Attention, à bien mettre le point (.) avec env. C’est bien .env .

Afin de pouvoir utiliser ce fichier .env sur toute notre application, nous devons dans notre fichier app.js, à la suite des importations des autres packages, codons : 

				
					require('dotenv').config();
				
			

Commençons par la chaîne de caractères d’identification à la base de données, que nous utilisons sur notre fichier app.js, pour nous connecter à la base de données.. 

Dans ce fichier .env, nous devons donc créer des paires clé/valeur, ou la clé aura le nom que l’on souhaite, et la valeur sera la donnée à sécuriser. 

Je vais donc écrire dans mon fichier .env, la clé MONGODB_PATH =

Vous pouvez mettre ce que vous voulez, mais en essayant de garder un sens tout de même, par rapport à la valeur que nous allons y attribuer.

Par convention, on met la clé en majuscules : Dans mon fichier .env, je code :

				
					MONGODB_PATH =
				
			

Ensuite, je vais aller sur mon fichier app.js, et vais récupérer la chaîne d’identification à ma base de données, en coupant cette chaîne et je vais la coller dans mon fichier .env, comme valeur de ma clé MONGODB_PATH  : 

				
					MONGODB_PATH = 'mongodb+srv://fabsolo:MotDePasse:cluster0.ktvxb.mongodb.net/NOMDEBASEDEDONNEES?retryWrites=true&w=majority'

				
			

Ensuite je retourne dans mon fichier app.js, et à la place de ma chaîne d’identification, j’écris process.env. , suivi de la clé que j’ai attribué, c’est à dire, pour cet exemple, j’écris process.env.MONGODB_PATH, et donc je code ainsi : 

				
					mongoose.connect(process.env.MONGODB_PATH,
{ useNewUrlParser: true,
   useUnifiedTopology: true })
   .then(() => console.log('Connexion à MongoDB réussie !'))
   .catch(() => console.log('Connexion à MongoDB échouée !'));

				
			

Continuons et sécurisons la clé de notre token

Dans le fichier .env, je met la clé TOKEN_KEY =

				
					MONGODB_PATH = 'mongodb+srv://fabsolo:MotDePasse:cluster0.ktvxb.mongodb.net/NOMDEBASEDEDONNEES?retryWrites=true&w=majority'
TOKEN_KEY =
				
			

Je vais ensuite couper la valeur de ma clé de token dans mon middleware auth.js :

				
					const decodedToken = jwt.verify(token, 'RANDOM_TOKEN_SECRET');
				
			

Ci-dessus, je coupe donc la clé de token : ‘RANDOM_TOKEN_SECRET’, et la colle comme valeur de la clé TOKEN_KEY dans mon fichier .env

				
					TOKEN_KEY = 'RANDOM_TOKEN_SECRET'
				
			

Ensuite, je retourne dans mon fichier auth.js , et je code à la place de ma clé de token

				
					const decodedToken = jwt.verify(token, process.env.TOKEN_KEY);
				
			

Et comme nous avons également utilisé cette clé de token, dans notre fonction login(), située dans notre contrôleur user.js, je vais également la remplacer .

Actuellement j’ai ceci dans ma fonction login() :

				
					res.status(200).json({
                        userId: user._id,
                        token: jwt.sign(
                            { userId: user._id },
                            'RANDOM_TOKEN_SECRET',
                            { expiresIn: '24h' }
                        )
                    });

				
			

Je remplace donc par : 

				
					res.status(200).json({
                        userId: user._id,
                        token: jwt.sign(
                            { userId: user._id },
                            process.env.TOKEN_KEY,
                            { expiresIn: '24h' }
                        )
                    });

				
			

Voilà comment utiliser dotenv, pour sécuriser nos données sensibles.

Bien entendu, à vous de poursuivre, et de créer des paires clé/valeur pour toutes les données que vous jugerez sensibles, lors de la création de vos projets. 

ATTENTION, afin que ce fichier .env, ne soit pas envoyé avec le reste de votre api sur GitHub, il faudra inscrire ce fichier .env, dans votre fichier .gitignore. Il sera alors ignoré lorsque vous ferez un “push” et un commit

Mon fichier .gitignore ressemblera alors à cela : 

				
					node_modules/
images/
.env

				
			

Je vous donne, toutefois, ma façon de procéder sur certains de mes projets. 

Il est tout de même utile de garder une trace de toutes les variables d’environnement à créer pour pouvoir lancer rapidement l’application après avoir cloné le repository.

Ma solution favorite est de créer un fichier .env.sample aux côtés de .env (à la racine du projet donc) et de le commit. Dans ce fichier je mets la même chose que dans .envsans les valeurs des variables !

Mon fichier .env.sample ressemblera donc à : 

				
					MONGODB_PATH =
TOKEN_KEY = 
				
			

Ainsi, si quelqu’un cloner le repository, il lui suffit de créer un fichier .env, et de mettre ces mêmes clés avec ses propres valeurs. Ces clés sont déjà présentes dans notre application, et cela évitera de devoir modifier le code dans les différentes fonctions. 

Conclusion de la série Création d’une API avec Node.js, Express et MongoDB

Nous voici arrivés au bout de cette série de tutoriels, qui est déjà très longue. 

De nombreuses autres améliorations peuvent être apportées à cette api, et je vous encourage vivement à continuer d’en apporter. 

Cette série étant déjà beaucoup plus longue que prévue au départ, je vais m’arrêter là. Nous avons déjà fait beaucoup de travail et nous avons une api fonctionnelle.

Si vous voulez quelques pistes pour continuer d’améliorer votre api, en voici quelques une : 

  • Implémenter un système de gestion de compte utilisateur (profil avec photo, etc.), c’est-à-dire un CRUD complet  pour ajouter des champs au schéma User.js, créer des fonctions dans le contrôleur user.js pour ajouter des informations au profil, modifier son profil, supprimer son compte, ajouter une photo, etc…
  • Implémenter un système de commentaires sur les avis de bières.
  • etc.

Les possibilités sont, bien entendu, infinies.

Il est maintenant l’heure pour moi, de vous souhaiter le meilleur pour vos futurs projets. 

Si cette série vous a plu, n’hésitez pas à me laisser vos commentaires, à partager, liker…

Pensez svp à prendre quelques secondes ou minutes pour vous inscrire sur Activateur Web, ou vous abonnez à ma page Facebook, ou à ma chaîne YouTube. N’hésitez pas à faire tout cela à la fois, cela m’encourage à continuer de produire ce type de contenu. 

Je vous donne rendez-vous pour de prochains tutoriels. Ceux qui seront inscrits sur Activateur web y auront accès en avant première

La suite de cette série, donnera lieu sûrement à des tutoriels comme :

  • Comment créer une documentation d’API avec Postman.
  • Création du frontend et connexion à l’API. 
  • Nouvelles améliorations de l’API.
  • etc. 

Tout ça dépendra de vos encouragements amis lecteurs. 

N’hésitez pas à me dire dans vos commentaires ce que vous aimeriez avoir comme futurs tutoriels. 

En attendant, prenez soin de vous et surtout restez curieux ! 

Code des fichiers de l'API

Fichiers à la racine du dossier Backend

server.js

				
					const http = require('http');
 
const app = require('./app');
 
const normalizePort = val => {
    const port = parseInt(val, 10);
 
    if(isNaN(port)) {
        return val;
    }
 
    if(port >= 0) {
        return port;
    }
 
    return false;
};
 
const port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
 
const errorHandler =error => {
    if(error.syscall !== 'listen') {
        throw error;
    }
 
    const address = server.address();
    const bind = typeof address === 'string'?'pipe' +address:'port:'+port;
    switch (error.code) {
        case 'EACCES':
            console.error(bind + 'requires elevated privileges.');
            process.exit(1);
            break;
        case 'EADDRINUSE':
            console.error(bind + 'is already in use.');
            process.exit(1);
            break;
        default:
            throw error;
    }
};
				
			

app.js

				
					const express = require('express');
const mongoose = require('mongoose');
const path = require('path');
const helmet = require('helmet');
require('dotenv').config();


const userRoutes = require('./routes/users');
const beerRoutes = require('./routes/beers');

const app = express();

mongoose.set('strictQuery', true);
mongoose.connect( process.env.MONGODB_PATH,
{
    useNewUrlParser: true,
    useUnifiedTopology: true
})
.then(() => console.log('Connexion à MongoDB réussie !'))
.catch(() => console.log('Connexion à MongoDB échouée !'));

app.use((req, res, next) => {
   res.setHeader('Access-Control-Allow-Origin', '*');
   res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content, Accept, Content-Type, Authorization');
   res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
   res.setHeader('Access-Control-Allow-Credentials', true);
   next();
}); 

app.use(helmet({
    crossOriginResourcePolicy: false,
}))

app.use(express.json());
app.use(express.urlencoded({ extended: true}));


app.use('/images', express.static(path.join(__dirname, 'images')));

app.use('/api/auth', userRoutes);
app.use('/api/beers', beerRoutes); 
 

module.exports = app;

				
			

Dossier controllers

beer.js

				
					const fs = require('fs');
const Beer = require('../models/Beer');

//; Créer (ajouter) une bière
exports.createBeer = (req, res, next) => {
    const beerObject = JSON.parse(req.body.beer);
    delete beerObject._id;
     const beer = new Beer({
        ...beerObject,
        imageUrl: `${req.protocol}://${req.get('host')}/images/${req.file.filename}`,
        likes: 0,
        dislikes: 0
     });
     beer.save()
        .then(() => res.status(201).json({ message: 'Objet enregistré avec succès !' }))
        .catch(error => res.status(400).json({ error }));
  };

  //; Récuperer toutes les bières
 exports.getAllBeers = (req, res, next) => {
    Beer.find()
       .then(beers => res.status(200).json(beers))
       .catch(error => res.status(400).json({ error }));
 };

 //; Récupérer une seule bière
 exports.getOneBeer = (req, res, next) => {
    Beer.findOne({ _id: req.params.id })
       .then(beer => res.status(200).json(beer))
       .catch(error => res.status(404).json({ error }));
 };
 
// ; Modifier et mettre à jour une bière
exports.modifyBeer = (req, res, next) => {
   if(req.file) {
      Beer.findOne({ _id: req.params.id })
         .then(beer => {
            const filename = beer.imageUrl.split('/images/')[1];
            fs.unlink(`images/${filename}`, () => {
               const beerObject = {
                  ...JSON.parse(req.body.beer),
                  imageUrl: `${req.protocol}://${req.get('host')}/images/${req.file.filename}`
               }
               Beer.updateOne({ _id: req.params.id }, { ...beerObject, _id: req.params.id })
                  .then(() => res.status(200).json({ message: 'La Bière a été modifiée avec succès !'}))
                  .catch(error => res.status(400).json({ error }));
            })
         })
         .catch(error => res.status(500).json({ error }));
   } else {
      const beerObject = { ...req.body };
      Beer.updateOne({ _id: req.params.id }, { ...beerObject, _id: req.params.id })
         .then(() => res.status(200).json({ message: 'Votre bière a été modifiée avec succès !'}))
         .catch(error => res.status(400).json({ error }));
   }
 };



//; Suppression d'une bière
exports.deleteBeer = (req, res, next) => {
   Beer.findOne({ _id: req.params.id})
      .then(beer => {
         const filename = beer.imageUrl.split('/images/')[1];
         fs.unlink(`images/${filename}`, () => {
            Beer.deleteOne({ _id: req.params.id })
            .then(() => res.status(200).json({ message: 'Votre bière a été supprimée avec succès !'}))
            .catch(error => res.status(400).json({ error }));
         });
      })
      .catch(error => res.status(500).json({ error }));
 };

 // ! Système de Like et Dislike
 
exports.likeBeer = (req, res, next) => {
   const userId = req.body.userId;
   const like = req.body.like;
   const beerId = req.params.id;
   Beer.findOne({ _id: beerId })
      .then(beer => {
         // nouvelles valeurs à Modifier
         const newValues = {
            usersLiked: beer.usersLiked,
            usersDisliked: beer.usersDisliked,
            likes: 0,
            dislikes: 0
         }
         // Suivant les cas :
         switch (like) {
            // Lorsque bière est "liké"
            case 1:
               newValues.usersLiked.push(userId);
               break;
            // Lorsque bière "disliké"
            case -1:
               newValues.usersDisliked.push(userId);
               break;
            // lorsque pas d'avis ou annulation du like ou dislike
            case 0:
               if(newValues.usersLiked.includes(userId)) {
                  // Si annulation du like
                  const index = newValues.usersLiked.indexOf(userId);
                  newValues.usersLiked.splice(index, 1);
               } else {
                  // Si annulation du Dislike
                  const index = newValues.usersDisliked.indexOf(userId);
                  newValues.usersDisliked.splice(index, 1);
               }
               break;
         };
         // Calcul du nombre de likes et de dislikes
         newValues.likes = newValues.usersLiked.length;
         newValues.dislikes = newValues.usersDisliked.length;
         // Mise à jour des nouvelles valeurs de la bière
         Beer.updateOne({ _id: beerId }, newValues )
            .then(() => res.status(200).json({ message: 'Note attribuée avec succès à cette bière !' }))
            .catch(error => res.status(500).json({ error }))
      })
      .catch(error => res.status(500).json({ error }));
};
 

				
			

user.js

				
					const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');

const User = require('../models/User');


exports.signup = (req, res, next) => {
    bcrypt.hash(req.body.password, 10)
        .then(hash => {
            const user = new User({
                email: req.body.email,
                password: hash
            });
            user.save()
                .then(() => res.status(201).json({ message: "Utilisateur créé avec succès !"}))
                .catch(error => res.status(400).json({ error }));
        })
        .catch(error => res.status(500).json({ error }));
};

exports.login = (req, res, next) => {
    User.findOne({ email: req.body.email })
         .then(user => {
             if(!user) {
                 return res.status(401).json({ error: "L\'utilisateur n\'existe pas ! "});
             }
             bcrypt.compare(req.body.password, user.password)
                 .then(valid => {
                     if(!valid) {
                         return res.status(401).json({ error: 'Mot de passe incorrect !' });
                     }
                     res.status(200).json({
                         userId: user._id,
                         token: jwt.sign(
                            { userId: user._id },
                            process.env.TOKEN_KEY,
                            { expiresIn: '24h'}
                         )
                     });
                 })
                 .catch(error => res.status(500).json({ error }));
         })
         .catch(error => res.status(500).json({ error }));
};
				
			

Dossier middleware

auth.js

				
					const jwt = require('jsonwebtoken');

module.exports = (req, res, next) => {
    try {
        const token = req.headers.authorization.split(' ')[1];
        const decodedToken = jwt.verify(token, process.env.TOKEN_KEY);
        const userId = decodedToken.userId;

        if( req.body.userId && req.body.userId !== userId ) {
            throw 'User ID non valable !';
        } else {
            next();
        }
    } catch {
        res.status(401).json({ message: 'Requête non authentifiée !' });
    }
};
				
			

multer-config.js

				
					const multer = require('multer');

const MIME_TYPES = {
    'image/jpg': 'jpg',
    'image/jpeg': 'jpg',
    'image/png': 'png',
};

const storage = multer.diskStorage({
    destination: (req, file, callback) => {
        callback(null, 'images');
    },
    filename: (req, file, callback) => {
        const name = file.originalname.split('.')[0].split(' ').join('_');
        const extension = MIME_TYPES[file.mimetype];
        callback(null, name + '_' + Date.now() + '.' + extension);
    }
});

module.exports = multer({ storage: storage }).single('image');
				
			

validate-inputs.js

				
					const Joi = require('joi');

const userSchema = Joi.object({
    email: Joi.string().trim().email().required(),
    password: Joi.string().trim().min(4).max(30).required()
});
exports.user = (req, res, next) => {
    const { error, value } = userSchema.validate(req.body);
    if(error) {
        res.status(422).json({ error: 'email ou mot de passe invalide !' });
    } else {
        next();
    }
};

//! Validation des données lors de l'ajout ou modification d'une bière
 
const beerSchema = Joi.object({
    userId: Joi.string().trim().length(24).required(),
    name: Joi.string().trim().min(2).required(),
    manufacturer: Joi.string().trim().min(2).required(),
    description: Joi.string().trim().min(5).required(),
    mainIngredient: Joi.string().trim().min(3).required(),
    degree: Joi.number().integer().min(1).max(10).required()
});
exports.beer = (req, res, next) => {
    let beer;
    if(req.file) {
        beer = JSON.parse(req.body.beer);
    } else {
        beer = req.body;
    }
   
    const { error, value } = beerSchema.validate(beer);
    if(error) {
        res.status(422).json({ error: 'Données invalides !' });
    } else {
        next();
    }
};

//! Validation de l'id des bières
const idSchema = Joi.string().trim().length(24).required();
exports.id = (req, res, next) => {
    const {error, value} = idSchema.validate(req.params.id);
    if(error) {
        res.status(422).json({ error: 'Id de la bière invalide !' });
    } else {
        next()
    }
};

//  Validation des likes/dislikes

const likeSchema = Joi.object({
    userId: Joi.string().trim().length(24).required(),
    like: Joi.valid(-1, 0, 1).required()
});
exports.like = (req, res, next) => {
    const { error, value } = likeSchema.validate(req.body);
    if(error) {
        res.status(422).json({ error: "Données renseignées invalides ! " });
    } else {
        next();
    }
};



				
			

Dossier models

Beer.js

				
					const mongoose = require('mongoose');

const beerSchema = mongoose.Schema({
    userId: { type: String, required: true },
    name: { type: String, required: true },
    manufacturer: { type: String, required: true },
    description: { type: String, required: true },
    mainIngredient: { type: String, required: true },
    imageUrl: { type: String, required: true },
    degree : { type: Number, required: true },
    likes: { type: Number, required: true },
    dislikes: { type: Number, required: true },
    usersLiked: { type: Array, required: true },
    usersDisliked: { type: Array, required: true },
});

module.exports = mongoose.model('Beer', beerSchema);
				
			

User.js

				
					const mongoose = require('mongoose');
const uniqueValidator = require('mongoose-unique-validator');

const userSchema = mongoose.Schema({
    email: { type: String, required: true, unique: true },
    password: { type: String, required: true }
});

userSchema.plugin(uniqueValidator);

module.exports = mongoose.model('User', userSchema);

				
			

Dossier routes

beers.js

				
					const express = require('express');
const router = express.Router();

const beerCtrl = require('../controllers/beer');
const auth = require('../middleware/auth');
const multer = require('../middleware/multer-config');
const validate = require('../middleware/validate-inputs');

router.post('/', auth, multer, validate.beer, beerCtrl.createBeer);
router.post('/:id/like', auth, validate.id, validate.like, beerCtrl.likeBeer);
router.get('/', auth, beerCtrl.getAllBeers);
router.get('/:id', auth, validate.id, beerCtrl.getOneBeer);
router.put('/:id', auth, multer, validate.id, validate.beer, beerCtrl.modifyBeer);
router.delete('/:id', auth, validate.id, beerCtrl.deleteBeer);

module.exports = router;
				
			

users.js

				
					const express = require('express');
const router = express.Router();

const userCtrl = require('../controllers/user');
const validate = require('../middleware/validate-inputs');

// Middleware pour s'incrire
router.post('/signup',validate.user,  userCtrl.signup );

// Middleware pour se connecter
router.post('/login', validate.user, userCtrl.login);

module.exports = router;
				
			

Une réponse

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Articles similaires

Développeur indépendant

Passionné par le développement web, j’aime créer les contenus web qui permettent à mes clients d’obtenir une marque, un style, un site à leur image.

Catégories
Les catégories d’articles
Mes Articles Préférés
Retrouvez Moi
Sur YouTube

Sur ma chaine Youtube, je partage avec vous sur différents sujets.

Sur Facebook
Liens Amis
Le Fouet Enchanté
Site e-commerce Le Fouet Enchanté
A découvrir

Connectez Vous à votre compte