Thierry  Perret

Thierry Perret

1657007880

Comment Créer Une API REST Avec Node.js, MySQL Et Express

Node.js est couramment utilisé avec des bases de données NoSQL telles que MongoDB, bien qu'il fonctionne également bien avec des bases de données relationnelles telles que MySQL, PostgreSQL et autres. MySQL existe depuis des décennies et est l'un des systèmes de gestion de bases de données relationnelles (RDBMS) les plus utilisés. La base de données la plus open source au monde est un SGBDR idéal pour les petites et les grandes applications.

Ce didacticiel explorera comment créer une API REST avec Express et MySQL en créant une base de données simple pour ajouter, mettre à jour et supprimer des tâches. Vous pouvez suivre en clonant le référentiel GitHub pour ce projet. Commençons!

Conditions préalables

Ce tutoriel est une démonstration pratique. Assurez-vous que les éléments suivants sont en place avant de commencer :

  1. Serveur MySQL installé sur votre ordinateur.
  2. Node.js installé sur votre ordinateur.

Toutes les démonstrations utiliseront Ubuntu 20.04 avec Node 14 LTS. Il fonctionne également pour d'autres systèmes d'exploitation et d'autres versions de Linux.

Qu'est-ce que le REPOS ?

REST (Representational State Transfer) est un style d'architecture logicielle que les développeurs ont établi pour aider à créer et à développer l'architecture du World Wide Web. REST spécifie un ensemble de critères sur la manière dont l'architecture d'un système hypermédia distribué à l'échelle d'Internet, tel que le Web, doit fonctionner. C'est l'une des différentes façons dont les applications, les serveurs et les sites Web peuvent communiquer des données et des services. Il fournit généralement les règles sur la façon dont les développeurs travaillant avec des données et des services représentent des éléments via l'API. D'autres programmes peuvent demander et recevoir de manière appropriée les données et les services qu'une API rend accessibles.

Pourquoi utiliser REST ?

REST possède plusieurs caractéristiques uniques qui en font l'un des meilleurs styles d'architecture logicielle largement utilisés par les ingénieurs en logiciel.

  1. Il est léger : les API REST utilisent le standard HTTP. Ainsi, vous pouvez utiliser XML, JSON, HTML et d'autres formats de données. Les API REST sont donc rapides et légères, ce qui en fait un excellent choix pour développer des applications mobiles, des appareils Internet des objets et d'autres applications.
  2. Elle est indépendante :  la séparation du client et du serveur dans une API REST la rend indépendante. Le protocole REST sépare le stockage des données et l'interface utilisateur du serveur, ce qui permet aux développeurs de collaborer facilement à la construction d'un projet.
  3. Il est évolutif et flexible : Les API REST sont évolutives et flexibles à utiliser en raison de la séparation du client et du serveur, permettant aux développeurs d'intégrer simplement les API REST sans aucun effort supplémentaire.
  4. Il est sans état : les appels sont effectués vers REST indépendamment, et chacun de ces appels contient toutes les données pour compléter la requête.
  5. Il possède une interface uniforme qui permet une évolution indépendante de l'application sans que les services ou modèles et actions de l'application ne soient étroitement couplés à la couche API elle-même.
  6. Il s'agit d'un système architectural en couches qui construit une hiérarchie qui aide à créer une application plus évolutive et modulaire.

Présentation des bases de données MySQL

MySQL est un système de gestion de base de données relationnelle (RDBMS) open source. C'est le système de base de données le plus utilisé avec PHP. MySQL est un service de base de données entièrement géré utilisé pour déployer des applications cloud natives. HeatWave, un accélérateur de requêtes hautes performances intégré, multiplie par 5 400 les performances de MySQL.

MySQL est développé, distribué et pris en charge par Oracle Corporation avec les fonctionnalités suivantes.

  • Les données d'une base de données MySQL résident dans des tables composées de colonnes et de lignes.
  • MySQL est un système de base de données qui s'exécute sur un serveur.
  • MySQL est idéal pour les petites et les grandes applications.
  • MySQL est un système de base de données très rapide, fiable et facile à utiliser. Il utilise le SQL standard.
  • MySQL compile sur plusieurs plates-formes.

Créer une application Node.js

Maintenant que nous avons exploré ce qu'est REST et que nous avons eu un aperçu rapide de la base de données MySQL, plongeons dans la création de notre application RESTFul. Tout d'abord, nous devons créer un dossier pour notre projet avec la commande ci-dessous :

mkdir rest-with-mysql && cd rest-with-mysql

Créer un nouveau dossier pour le projet

La commande ci-dessus créera un dossier rest-with-MySQL et changera le répertoire dans le dossier. Ensuite, nous allons initialiser un nouveau projet node.js avec la commande ci-dessous :

npm init -y

Initialiser un nouveau projet Node.js

La commande ci-dessus initialisera un nouveau projet Node.js, en ignorant toutes les invites pour les détails du projet. La commande créera également un fichier package.json dans le répertoire racine de notre projet, qui enregistrera toutes les métadonnées essentielles sur notre projet. Vous pouvez décider de passer par les invites pour entrer les détails du projet en supprimant le drapeau de la commande.

Ensuite, nous allons installer les dépendances dont nous avons besoin pour notre projet.

npm install express MySQL cors

Installation de Cors

La commande ci-dessus prendra un peu de temps, mais nous aurons installé Express , MySQL et Cors à la fin. Enfin, regardons la structure de notre projet. À la fin de ce didacticiel, notre structure de projet ressemblera à ceci :

La structure de notre application.

Structuration des candidatures.

Configuration du serveur Express

Maintenant que nos dépendances sont installées, mettons-les au travail en configurant d'abord notre serveur express.

Créez un fichier app.js et ajoutez-y l'extrait de code suivant ci-dessous. Nous allons importer les éléments suivants :

  • Express : Pour créer notre serveur.
  • Cors : pour autoriser et rediriger les ressources de requête.
  • Routeur : C'est là que nos routes API seront définies plus tard dans les sections.
  • AppError et errorHandler : ce sont nos fonctions globales de gestion des erreurs. Nous les créerons plus tard.
const express = require("express");
const cors = require("cors");
const router = require("./routes");
const AppError = require("./utils/appError");
const errorHandler = require("./utils/errorHandler");
….

Mise en place de nos dépendances.

Ensuite, nous créons une instance d' application à partir d'express, en utilisant le middleware express.json() dans notre application pour analyser le corps encodé de l'URL. Enfin, nous faisons en sorte que notre middleware de routeur d'API écoute les requêtes entrantes à l'URL spécifiée.

Ensuite, nous recherchons les URL manquantes sur nos points de terminaison et renvoyons une erreur 404 à l'utilisateur s'il y accède. Le gestionnaire d'erreurs global s'en chargera (nous le créerons dans les sections suivantes).

app.use(api, router);

app.all("*", (req, res, next) => {
 next(new AppError(`The URL ${req.originalUrl} does not exists`, 404));
});
app.use(errorHandler);
...

Vérifier les points de terminaison de la mission et diriger les utilisateurs vers un 404 s'ils en atteignent un.

Enfin, nous configurerons notre application pour écouter le port 3000.

const PORT = 3000;
app.listen(PORT, () => {
 console.log(`server running on port ${PORT}`);
});

module.exports = app;

Choisir le port 3000 pour notre port d'écoute.

Configurer et se connecter à MySQL

Maintenant, nous avons configuré notre serveur Express. Allons-y et configurons notre base de données MySQL. Tout d'abord, nous allons ouvrir notre shell MySQL avec la commande ci-dessous :

//On Windows
MySQL

//Ubuntu
mysql -u root -p

Ouverture de la coque

La commande ci-dessus vous demandera votre mot de passe root.

Capture d'écran d'une invite de mot de passe.

Utilisez votre mot de passe pour continuer.

Saisissez le mot de passe et appuyez sur la touche Entrée pour continuer. Si tout se passe bien, vous devriez voir une sortie similaire à la capture d'écran ci-dessous sur votre terminal.

Capture d'écran de la coque.

Succès! C'est ce que vous verrez si tout se passe bien.

Ensuite, exécutez les instructions SQL ci-dessous sur votre shell MySQL pour créer notre base de données todos .

CREATE DATABASE todos

Créez la base de données de tâches.

Ensuite, exécutez la commande ci-dessous pour créer notre table todolist . La table aura un identifiant , un nom, un statut, des champs date_created   . L'identifiant déposé sera la clé primaire de notre table.

CREATE TABLE todolist(id int NOT NULL AUTO_INCREMENT,
name varchar(50) NOT NULL, 
status varchar(50), 
date_created DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 
PRIMARY KEY (id));

Initialisation de la table todolist.

Ensuite, créez un dossier de services dans le répertoire racine de notre projet. Dans le dossier services , créez un fichier db.js et ajoutez-y les extraits de code suivants ci-dessous.

const mysql = require('mysql');
const conn = mysql.createConnection({
 host: "localhost",
 user: "root",
 password: "1234",
 database: "todos",
});

conn.connect();

module.exports = conn;

Connexion à la base de données MySQL avec la méthode createConnection.

Le code ci-dessus se connectera à notre base de données MySQL en utilisant la méthode MySQL createConnection . La méthode createConnection prend l' hôte , le nom d' utilisateur , le mot de passe et le nom de la base de données comme paramètres obligatoires.

Création de contrôleurs d'application

Nous avons connecté avec succès notre base de données MySQL. Passons à la création des routes pour notre application.

Dans le répertoire racine de notre projet, créez un dossier controllers , puis créez un fichier index.js dans le dossier controllers .

Tout d'abord, nous allons importer notre gestionnaire d'erreurs global et notre connexion à la base de données MySQL.

const AppError = require("../utils/appError");
const conn = require("../services/db");

Importation du gestionnaire d'erreurs global.

Ensuite, nous allons créer notre gestionnaire getAllTodos pour récupérer toutes les tâches de notre base de données. Ce gestionnaire utilisera la méthode de requête MySQL, qui prend une requête SQL et une fonction de rappel comme paramètres. Si une erreur se produit pendant l'opération, nous renverrons l'erreur à l'utilisateur à l'aide de la classe AppError . Ensuite, nous renvoyons les données à l'utilisateur lorsque l'opération s'exécute avec succès avec un code d' état 200 .

exports.getAllTodos = (req, res, next) => {
 conn.query("SELECT * FROM todolist", function (err, data, fields) {
   if(err) return next(new AppError(err))
   res.status(200).json({
     status: "success",
     length: data?.length,
     data: data,
   });
 });
};

Le gestionnaire getAllTodos.

Ensuite, nous allons créer notre gestionnaire createTodo pour ajouter de nouvelles tâches à notre base de données. Ensuite, nous vérifions si le client envoie un formulaire vide et renvoyons un message d'erreur 404 .

Ensuite, nous obtenons le nom de la tâche à partir du corps de la requête et définissons le statut de chaque tâche créée sur en attente par défaut. En utilisant la méthode query mysql, nous créons une requête d'insertion pour ajouter la tâche à notre base de données.

exports.createTodo = (req, res, next) => {
 if (!req.body) return next(new AppError("No form data found", 404));
 const values = [req.body.name, "pending"];
 conn.query(
   "INSERT INTO todolist (name, status) VALUES(?)",
   [values],
   function (err, data, fields) {
     if (err) return next(new AppError(err, 500));
     res.status(201).json({
       status: "success",
       message: "todo created!",
     });
   }
 );
};

Le gestionnaire createTodo

Ensuite, nous créons un gestionnaire getTodo pour obtenir nos tâches par ID. Tout d'abord, nous vérifierons si l'id est spécifié dans le paramètre de requête, puis renverrons une erreur au client si aucun n'a été trouvé.

Nous allons exécuter une instruction SQL pour obtenir la tâche dont l'identifiant est sur le paramètre de requête et la renvoyer au client.

exports.getTodo = (req, res, next) => {
 if (!req.params.id) {
   return next(new AppError("No todo id found", 404));
 }
 conn.query(
   "SELECT * FROM todolist WHERE id = ?",
   [req.params.id],
   function (err, data, fields) {
     if (err) return next(new AppError(err, 500));
     res.status(200).json({
       status: "success",
       length: data?.length,
       data: data,
     });
   }
 );
};

Le gestionnaire getTodo.

Ensuite, nous allons créer notre gestionnaire updateTodo pour mettre à jour nos tâches, cette fois-ci modifiera la tâche dont l' id est dans le paramètre de requête à compléter.

exports.updateTodo = (req, res, next) => {
 if (!req.params.id) {
   return next(new AppError("No todo id found", 404));
 }
 conn.query(
   "UPDATE todolist SET status='completed' WHERE id=?",
   [req.params.id],
   function (err, data, fields) {
     if (err) return next(new AppError(err, 500));
     res.status(201).json({
       status: "success",
       message: "todo updated!",
     });
   }
 );
};

Le gestionnaire updateTodo

Enfin, nous allons créer un gestionnaire deleteTodo pour supprimer une tâche de notre base de données. Nous allons exécuter une instruction delete pour supprimer une tâche dont l' id est le paramètre de la requête.

exports.deleteTodo = (req, res, next) => {
 if (!req.params.id) {
   return next(new AppError("No todo id found", 404));
 }
 conn.query(
   "DELETE FROM todolist WHERE id=?",
   [req.params.id],
   function (err, fields) {
     if (err) return next(new AppError(err, 500));
     res.status(201).json({
       status: "success",
       message: "todo deleted!",
     });
   }
 );
}

Le gestionnaire deleteTodo

Création de gestionnaires d'erreurs globaux

Maintenant, créons rapidement nos gestionnaires d'erreurs globaux. Pour commencer, créez un dossier utils dans le répertoire racine de notre projet. Créez ensuite les fichiers appError.js et errorHandler.js . Ajoutez l'extrait de code suivant ci-dessous au fichier appError.js .

class AppError extends Error {
 constructor(msg, statusCode) {
   super(msg);

   this.statusCode = statusCode;
   this.error = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
   this.isOperational = true;

   Error.captureStackTrace(this, this.constructor);
 }
}
module.exports = AppError;

Le fichier appError.js.

Le code ci-dessus crée une classe AppError qui étend la classe Error intégrée . Nous transmettrons ensuite le message d'erreur et l' état au constructeur de la classe Error. Ensuite, nous vérifierons quel type d'erreur s'est produit dans notre application avec le début du code d'état et ajouterons l'erreur à la trace de la pile d'erreurs.

Ensuite, ouvrez le fichier errorHandler.js et ajoutez l'extrait de code ci-dessous :

module.exports = (err, req, res, next) => {
 err.statusCode = err.statusCode || 500;
 err.status = err.status || "error";
 res.status(err.statusCode).json({
   status: err.status,
   message: err.message,
 });
};

Le fichier errorHandler.js

Le code ci-dessus vérifiera les éventuelles erreurs dans notre application et enverra l'erreur et le code d'état correspondants au client sans interrompre notre application.

Création de routes applicatives

Dans le répertoire racine de notre projet, créez un dossier routes , puis créez un fichier index.js et ajoutez-y l'extrait de code suivant ci-dessous.

const express = require("express");
const controllers = require("../controllers");
const router = express.Router();

router.route("/").get(controllers.getAllTodos).post(controllers.createTodo);
router
 .route("/:id")
 .get(controllers.getTodo)
 .put(controllers.updateTodo)
 .delete(controllers.deleteTodo);
module.exports = router;

Routes pour l'application.

Le code ci-dessus crée un objet routeur à partir de la classe de routeur express. Ensuite, nous faisons les itinéraires suivants dans notre application.

  • Get Route : pour obtenir toutes les tâches dans notre base de données.
  • Post Route : pour ajouter une nouvelle tâche à notre base de données
  • Get Route : pour obtenir une tâche par son identifiant
  • Put Route : pour mettre à jour une tâche par l'identifiant
  • Supprimer l'itinéraire : pour supprimer une tâche par l'identifiant.

Ensuite, nous exportons l'objet routeur.

Afficher les données avec Arctype

Nous avons maintenant créé tous nos itinéraires. Connectons-nous maintenant à Arctype pour visualiser les données de notre base de données. Pour ce faire, nous devons télécharger Arctype depuis le site officiel. Téléchargez et installez Arctype, puis connectez-y votre base de données. Lorsque vous lancez Arctype, cliquez sur MySQL pour créer une connexion - suivez ce guide si vous avez besoin d'aide.

Image d'Arctype au lancement.

Lancement d'Arctype et création de la connexion.

Ensuite, nous allons ajouter nos identifiants de connexion et appuyer sur le bouton Enregistrer .

Capture d'écran de l'ajout d'informations d'identification dans Arctype.

Ajout de nos identifiants dans Arctype .

Nous nous sommes connectés avec succès à notre base de données. Vous devriez voir nos tables de base de données sur le côté droit du tableau de bord Arctype. Cliquez sur la table todolist pour exécuter des requêtes sur notre base de données sur Arctype.

Nous pouvons ajouter un nouvel enregistrement à notre base de données en cliquant sur l' onglet Insérer une ligne .

Capture d'écran de la fonction d'insertion de lignes dans Arctype.

Ajout de nouveaux enregistrements via l'onglet Insérer une ligne dans Arctype .

Nous pouvons également modifier et interroger notre table de base de données à l'aide d'Arctype... et plus encore.

Conclusion

Vous comprenez maintenant comment créer une API REST à l'aide du framework Express : installez le package Express Node.js, créez une base de données MySQL et créez une application de tâches simple. C'est tout ce qu'il faut!

Lien : https://arctype.com/blog/rest-api-tutorial/

#node  #nodejs  #restapi #mysql #express 

What is GEEK

Buddha Community

Comment Créer Une API REST Avec Node.js, MySQL Et Express
Thierry  Perret

Thierry Perret

1651721469

Comment Créer Une API REST Avec JavaScript, Node.js Et Express.js

J'ai créé et utilisé de nombreuses API au cours des dernières années. Pendant ce temps, j'ai rencontré de bonnes et de mauvaises pratiques et j'ai vécu des situations désagréables lors de la consommation et de la création d'API. Mais il y a aussi eu de grands moments.

Il existe des articles utiles en ligne qui présentent de nombreuses meilleures pratiques, mais beaucoup d'entre eux manquent de praticité à mon avis. Connaître la théorie avec quelques exemples est une bonne chose, mais je me suis toujours demandé à quoi ressemblerait la mise en œuvre dans un exemple plus réel.

Fournir des exemples simples aide à comprendre le concept lui-même sans trop de complexité, mais dans la pratique, les choses ne sont pas toujours aussi simples. Je suis sûr que vous savez de quoi je parle 😁

C'est pourquoi j'ai décidé d'écrire ce tutoriel. J'ai fusionné tous ces apprentissages (bons et mauvais) en un seul article digeste tout en fournissant un exemple pratique qui peut être suivi. En fin de compte, nous créerons une API complète tout en mettant en œuvre les meilleures pratiques les unes après les autres.

Quelques points à retenir avant de commencer :

Comme vous l'avez peut-être deviné, les meilleures pratiques ne sont pas des lois ou des règles spécifiques à suivre. Ce sont des conventions ou des astuces qui ont évolué au fil du temps et qui se sont révélées efficaces. Certains sont devenus la norme de nos jours. Mais cela ne signifie pas que vous devez les adapter 1:1.

Ils devraient vous donner une direction pour améliorer votre API en termes d'expérience utilisateur (pour le consommateur et le constructeur), de sécurité et de performances.

Gardez simplement à l'esprit que les projets sont différents et nécessitent des approches différentes. Il peut y avoir des situations où vous ne pouvez pas ou ne devriez pas suivre une certaine convention. Donc, chaque ingénieur doit décider cela pour lui-même ou avec le sien.

Maintenant que nous avons réglé ces choses, sans plus tarder, mettons-nous au travail !

Notre exemple de projet

alvaro-reyes-qWwpHwip31M-unsplash--1-

Photo par Alvaro Reyes sur Unsplash

Avant de commencer à mettre en œuvre les meilleures pratiques dans notre exemple de projet, j'aimerais vous donner une brève introduction à ce que nous allons construire.

Nous allons créer une API REST pour une application d'entraînement CrossFit. Si vous n'êtes pas familier avec le CrossFit, c'est une méthode de fitness et un sport de compétition qui combine des entraînements de haute intensité avec des éléments de plusieurs sports (haltérophilie olympique, gymnastique et autres).

Dans notre application, nous aimerions créer, lire, mettre à jour et supprimer des WOD ( W orkouts of the Day ). Cela aidera nos utilisateurs (c'est-à-dire les propriétaires de salles de sport) à élaborer des plans d'entraînement et à maintenir leurs propres entraînements dans une seule application. En plus de cela, ils peuvent également ajouter des conseils d'entraînement importants pour chaque entraînement.

Notre travail nous obligera à concevoir et à implémenter une API pour cette application.

Conditions préalables

Pour suivre, vous devez avoir une certaine expérience en JavaScript, Node.js, Express.js et en architecture backend. Des termes tels que REST et API ne devraient pas être nouveaux pour vous et vous devriez avoir une compréhension du Client-Server-Model .

Bien sûr, vous n'avez pas besoin d'être un expert dans ces domaines, mais la familiarité et idéalement une certaine expérience devraient suffire.

Si tous les prérequis ne s'appliquent pas à vous, ce n'est bien sûr pas une raison pour sauter ce tutoriel. Il y a encore beaucoup à apprendre ici pour vous aussi. Mais avoir ces compétences vous permettra de suivre plus facilement.

Même si cette API est écrite en JavaScript et Express, les meilleures pratiques ne se limitent pas à ces outils. Ils peuvent également être appliqués à d'autres langages de programmation ou frameworks.

Architecture

Comme indiqué ci-dessus, nous utiliserons Express.js pour notre API. Je ne veux pas proposer une architecture complexe, j'aimerais donc m'en tenir à l' architecture à 3 couches :

Capture d'écran-2022-04-25-at-14.33.24-1

À l'intérieur du contrôleur , nous gérerons tout ce qui est lié à HTTP. Cela signifie que nous traitons des demandes et des réponses pour nos terminaux. Au-dessus de cette couche se trouve également un petit routeur d'Express qui transmet les requêtes au contrôleur correspondant.

Toute la logique métier se trouvera dans la couche de service qui exporte certains services (méthodes) utilisés par le contrôleur.

La troisième couche est la couche d'accès aux données où nous travaillerons avec notre base de données. Nous allons exporter certaines méthodes pour certaines opérations de base de données comme la création d'un WOD qui peut être utilisé par notre Service Layer.

Dans notre exemple, nous n'utilisons pas une vraie base de données telle que MongoDB ou PostgreSQL car j'aimerais me concentrer davantage sur les meilleures pratiques elles-mêmes. Par conséquent, nous utilisons un fichier JSON local qui imite notre base de données. Mais cette logique peut être transférée à d'autres bases de données bien sûr.

Configuration de base

Nous devrions maintenant être prêts à créer une configuration de base pour notre API. Nous ne compliquerons pas trop les choses et nous construirons une structure de projet simple mais organisée.

Commençons par créer la structure globale des dossiers avec tous les fichiers et dépendances nécessaires. Après cela, nous ferons un test rapide pour vérifier si tout fonctionne correctement :

# Create project folder & navigate into it
mkdir crossfit-wod-api && cd crossfit-wod-api
# Create a src folder & navigate into it
mkdir src && cd src
# Create sub folders
mkdir controllers && mkdir services && mkdir database && mkdir routes
# Create an index file (entry point of our API)
touch index.js
# We're currently in the src folder, so we need to move one level up first 
cd .. 

# Create package.json file 
npm init -y

Installez les dépendances pour la configuration de base :

# Dev Dependencies 
npm i -D nodemon 

# Dependencies 
npm i express

Ouvrez le projet dans votre éditeur de texte préféré et configurez Express :

// In src/index.js 
const express = require("express"); 

const app = express(); 
const PORT = process.env.PORT || 3000; 

// For testing purposes 
app.get("/", (req, res) => { 
    res.send("<h2>It's Working!</h2>"); 
}); 

app.listen(PORT, () => { 
    console.log(`API is listening on port ${PORT}`); 
});

Intégrez un nouveau script appelé "dev" dans package.json :

{
  "name": "crossfit-wod-api",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "nodemon src/index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "nodemon": "^2.0.15"
  },
  "dependencies": {
    "express": "^4.17.3"
  }
}

Le script s'assure que le serveur de développement redémarre automatiquement lorsque nous apportons des modifications (grâce à nodemon).

Lancez le serveur de développement :

npm run dev

Regardez votre terminal, et il devrait y avoir un message indiquant que "l'API écoute sur le port 3000" .

Visitez localhost:3000 dans votre navigateur. Lorsque tout est correctement configuré, vous devriez voir ce qui suit :

Capture d'écran-2022-04-30-at-11.09.44

Génial! Nous sommes maintenant prêts à mettre en œuvre les meilleures pratiques.

Meilleures pratiques de l'API REST

constantin-wenning-idDvA4jPBO8-unsplash--1-

Photo de Constantin Wenning sur Unsplash

Ouais! Maintenant que nous avons une configuration Express vraiment basique, nous pouvons étendre notre API avec les meilleures pratiques suivantes.

Commençons simplement avec nos points de terminaison CRUD fondamentaux. Après cela, nous étendrons l'API avec chaque meilleure pratique.

Gestion des versions

Attends une seconde. Avant d'écrire un code spécifique à l'API, nous devons être conscients de la gestion des versions. Comme dans d'autres applications, il y aura des améliorations, de nouvelles fonctionnalités et des choses comme ça. Il est donc important de versionner également notre API.

Le gros avantage est que nous pouvons travailler sur de nouvelles fonctionnalités ou des améliorations sur une nouvelle version pendant que les clients utilisent toujours la version actuelle et ne sont pas affectés par les changements de rupture.

Nous ne forçons pas non plus les clients à utiliser la nouvelle version immédiatement. Ils peuvent utiliser la version actuelle et migrer eux-mêmes lorsque la nouvelle version est stable.

Les versions actuelles et nouvelles fonctionnent essentiellement en parallèle et ne s'affectent pas les unes les autres.

Mais comment différencier les versions ? Une bonne pratique consiste à ajouter un segment de chemin comme v1 ou v2 dans l'URL.

// Version 1 
"/api/v1/workouts" 

// Version 2 
"/api/v2/workouts" 

// ...

C'est ce que nous exposons au monde extérieur et ce qui peut être consommé par d'autres développeurs. Mais nous devons également structurer notre projet afin de différencier chaque version.

Il existe de nombreuses approches différentes pour gérer la gestion des versions dans une API Express. Dans notre cas, j'aimerais créer un sous-dossier pour chaque version dans notre répertoire src appelé v1 .

mkdir src/v1

Maintenant, nous déplaçons notre dossier routes dans ce nouveau répertoire v1.

# Get the path to your current directory (copy it) 
pwd 

# Move "routes" into "v1" (insert the path from above into {pwd}) 
mv {pwd}/src/routes {pwd}/src/v1

Le nouveau répertoire /src/v1/routes stockera toutes nos routes pour la version 1. Nous ajouterons du "vrai" contenu plus tard. Mais pour l'instant, ajoutons un simple fichier index.js pour tester les choses.

# In /src/v1/routes 
touch index.js

À l'intérieur, nous faisons tourner un simple routeur.

// In src/v1/routes/index.js
const express = require("express");
const router = express.Router();

router.route("/").get((req, res) => {
  res.send(`<h2>Hello from ${req.baseUrl}</h2>`);
});

module.exports = router;

Nous devons maintenant connecter notre routeur pour la v1 à notre point d'entrée racine dans src/index.js.

// In src/index.js
const express = require("express");
// *** ADD ***
const v1Router = require("./v1/routes");

const app = express();
const PORT = process.env.PORT || 3000;

// *** REMOVE ***
app.get("/", (req, res) => {
  res.send("<h2>It's Working!</h2>");
});

// *** ADD ***
app.use("/api/v1", v1Router);

app.listen(PORT, () => {
  console.log(`API is listening on port ${PORT}`);
});

Visitez maintenant localhost:3000/api/v1 dans votre navigateur et vous devriez voir ce qui suit :

Capture d'écran-2022-04-30-at-11.22.28

Toutes nos félicitations! Vous venez de structurer le projet pour gérer différentes versions. Nous transmettons maintenant les requêtes entrantes avec "/api/v1" à notre routeur version 1, qui acheminera chaque requête vers la méthode de contrôleur correspondante ultérieurement.

Avant de poursuivre, j'aimerais souligner quelque chose.

Nous venons de déplacer notre dossier routes dans notre répertoire v1. Les autres dossiers comme les contrôleurs ou les services restent toujours dans notre répertoire src. Ce n'est pas grave pour le moment, car nous construisons une API plutôt petite. Nous pouvons utiliser les mêmes contrôleurs et services dans chaque version à l'échelle mondiale.

Lorsque l'API se développe et nécessite différentes méthodes de contrôleur spécifiques à la v2, par exemple, il serait préférable de déplacer également le dossier des contrôleurs dans le répertoire v2 pour que toute la logique spécifique à cette version particulière soit encapsulée.

Une autre raison à cela pourrait être que nous pourrions modifier un service utilisé par toutes les autres versions. Nous ne voulons pas casser des choses dans les autres versions. Il serait donc judicieux de déplacer également le dossier des services dans un dossier de version spécifique.

Mais comme je l'ai dit, dans notre exemple, je peux uniquement différencier les routes et laisser le routeur gérer le reste. Néanmoins, il est important de garder cela à l'esprit pour avoir une structure claire lorsque l'API évolue et nécessite des modifications.

Nom des ressources au pluriel

Après avoir tout configuré, nous pouvons maintenant nous plonger dans la véritable implémentation de notre API. Comme je l'ai dit, j'aimerais commencer par nos points de terminaison CRUD fondamentaux.

En d'autres termes, commençons à implémenter des points de terminaison pour créer, lire, mettre à jour et supprimer des entraînements.

Tout d'abord, connectons un contrôleur, un service et un routeur spécifiques pour nos entraînements.

touch src/controllers/workoutController.js 

touch src/services/workoutService.js 

touch src/v1/routes/workoutRoutes.js

J'aime toujours commencer par les itinéraires en premier. Réfléchissons à la façon dont nous pouvons nommer nos terminaux. Cela va de pair avec cette pratique exemplaire particulière.

Nous pourrions nommer le point de terminaison de création /api/v1/workout car nous aimerions ajouter un entraînement, n'est-ce pas ? Fondamentalement, il n'y a rien de mal à cette approche - mais cela peut conduire à des malentendus.

Rappelez-vous toujours : votre API est utilisée par d'autres humains et doit être précise. Cela vaut également pour nommer vos ressources.

J'imagine toujours une ressource comme une boîte. Dans notre exemple, la boîte est une collection qui stocke différents entraînements .

Nommer vos ressources au pluriel a le gros avantage qu'il est parfaitement clair pour les autres humains, qu'il s'agit d'une collection qui se compose de différentes séances d'entraînement.

Alors, définissons nos points de terminaison dans notre routeur d'entraînement.

// In src/v1/routes/workoutRoutes.js
const express = require("express");
const router = express.Router();

router.get("/", (req, res) => {
  res.send("Get all workouts");
});

router.get("/:workoutId", (req, res) => {
  res.send("Get an existing workout");
});

router.post("/", (req, res) => {
  res.send("Create a new workout");
});

router.patch("/:workoutId", (req, res) => {
  res.send("Update an existing workout");
});

router.delete("/:workoutId", (req, res) => {
  res.send("Delete an existing workout");
});

module.exports = router;

Vous pouvez supprimer notre fichier de test index.js dans src/v1/routes .

Passons maintenant à notre point d'entrée et connectons notre routeur d'entraînement v1.

// In src/index.js
const express = require("express");
// *** REMOVE ***
const v1Router = require("./v1/routes");
// *** ADD ***
const v1WorkoutRouter = require("./v1/routes/workoutRoutes");

const app = express();
const PORT = process.env.PORT || 3000;

// *** REMOVE ***
app.use("/api/v1", v1Router);

// *** ADD ***
app.use("/api/v1/workouts", v1WorkoutRouter);

app.listen(PORT, () => {
  console.log(`API is listening on port ${PORT}`);
});

Cela s'est bien passé, non ? Maintenant, nous interceptons toutes les requêtes qui vont à /api/v1/workouts avec notre v1WorkoutRouter.

À l'intérieur de notre routeur, nous appellerons une méthode différente gérée par notre contrôleur pour chaque point de terminaison différent.

Créons une méthode pour chaque point de terminaison. Le simple fait de renvoyer un message devrait suffire pour le moment.

// In src/controllers/workoutController.js
const getAllWorkouts = (req, res) => {
  res.send("Get all workouts");
};

const getOneWorkout = (req, res) => {
  res.send("Get an existing workout");
};

const createNewWorkout = (req, res) => {
  res.send("Create a new workout");
};

const updateOneWorkout = (req, res) => {
  res.send("Update an existing workout");
};

const deleteOneWorkout = (req, res) => {
  res.send("Delete an existing workout");
};

module.exports = {
  getAllWorkouts,
  getOneWorkout,
  createNewWorkout,
  updateOneWorkout,
  deleteOneWorkout,
};

Il est maintenant temps de refactoriser un peu notre routeur d'entraînement et d'utiliser les méthodes du contrôleur.

// In src/v1/routes/workoutRoutes.js
const express = require("express");
const workoutController = require("../../controllers/workoutController");

const router = express.Router();

router.get("/", workoutController.getAllWorkouts);

router.get("/:workoutId", workoutController.getOneWorkout);

router.post("/", workoutController.createNewWorkout);

router.patch("/:workoutId", workoutController.updateOneWorkout);

router.delete("/:workoutId", workoutController.deleteOneWorkout);

module.exports = router;

Nous pouvons maintenant tester notre point de terminaison GET /api/v1/workouts/:workoutId en tapant localhost:3000/api/v1/workouts/2342 dans le navigateur. Vous devriez voir quelque chose comme ceci :

Capture d'écran-2022-04-30-at-11.29.19

Nous l'avons fait ! La première couche de notre architecture est terminée. Créons notre couche de service en mettant en œuvre la meilleure pratique suivante.

Accepter et répondre avec des données au format JSON

Lorsque vous interagissez avec une API, vous envoyez toujours des données spécifiques avec votre demande ou vous recevez des données avec la réponse. Il existe de nombreux formats de données différents, mais JSON (Javascript Object Notation) est un format standardisé.

Bien qu'il y ait le terme JavaScript dans JSON, il n'y est pas spécifiquement lié. Vous pouvez également écrire votre API avec Java ou Python qui peut également gérer JSON.

En raison de sa standardisation, les API doivent accepter et répondre avec des données au format JSON.

Examinons notre implémentation actuelle et voyons comment nous pouvons intégrer cette meilleure pratique.

Tout d'abord, nous créons notre couche de service.

// In src/services/workoutService.js
const getAllWorkouts = () => {
  return;
};

const getOneWorkout = () => {
  return;
};

const createNewWorkout = () => {
  return;
};

const updateOneWorkout = () => {
  return;
};

const deleteOneWorkout = () => {
  return;
};

module.exports = {
  getAllWorkouts,
  getOneWorkout,
  createNewWorkout,
  updateOneWorkout,
  deleteOneWorkout,
};

C'est également une bonne pratique de nommer les méthodes de service de la même manière que les méthodes de contrôleur afin que vous ayez une connexion entre celles-ci. Commençons par ne rien renvoyer.

À l'intérieur de notre contrôleur d'entraînement, nous pouvons utiliser ces méthodes.

// In src/controllers/workoutController.js
// *** ADD ***
const workoutService = require("../services/workoutService");

const getAllWorkouts = (req, res) => {
  // *** ADD ***
  const allWorkouts = workoutService.getAllWorkouts();
  res.send("Get all workouts");
};

const getOneWorkout = (req, res) => {
  // *** ADD ***
  const workout = workoutService.getOneWorkout();
  res.send("Get an existing workout");
};

const createNewWorkout = (req, res) => {
  // *** ADD ***
  const createdWorkout = workoutService.createNewWorkout();
  res.send("Create a new workout");
};

const updateOneWorkout = (req, res) => {
  // *** ADD ***
  const updatedWorkout = workoutService.updateOneWorkout();
  res.send("Update an existing workout");
};

const deleteOneWorkout = (req, res) => {
  // *** ADD ***
  workoutService.deleteOneWorkout();
  res.send("Delete an existing workout");
};

module.exports = {
  getAllWorkouts,
  getOneWorkout,
  createNewWorkout,
  updateOneWorkout,
  deleteOneWorkout,
};

Pour le moment, rien n'aurait dû changer dans nos réponses. Mais sous le capot, notre couche contrôleur parle maintenant avec notre couche service.

Dans nos méthodes de service, nous gérerons notre logique métier, comme la transformation des structures de données et la communication avec notre couche de base de données.

Pour ce faire, nous avons besoin d'une base de données et d'un ensemble de méthodes qui gèrent réellement l'interaction avec la base de données. Notre base de données sera un simple fichier JSON qui est déjà pré-rempli avec quelques entraînements.

# Create a new file called db.json inside src/database 
touch src/database/db.json 

# Create a Workout File that stores all workout specific methods in /src/database 
touch src/database/Workout.js

Copiez ce qui suit dans db.json :

{
  "workouts": [
    {
      "id": "61dbae02-c147-4e28-863c-db7bd402b2d6",
      "name": "Tommy V",
      "mode": "For Time",
      "equipment": [
        "barbell",
        "rope"
      ],
      "exercises": [
        "21 thrusters",
        "12 rope climbs, 15 ft",
        "15 thrusters",
        "9 rope climbs, 15 ft",
        "9 thrusters",
        "6 rope climbs, 15 ft"
      ],
      "createdAt": "4/20/2022, 2:21:56 PM",
      "updatedAt": "4/20/2022, 2:21:56 PM",
      "trainerTips": [
        "Split the 21 thrusters as needed",
        "Try to do the 9 and 6 thrusters unbroken",
        "RX Weights: 115lb/75lb"
      ]
    },
    {
      "id": "4a3d9aaa-608c-49a7-a004-66305ad4ab50",
      "name": "Dead Push-Ups",
      "mode": "AMRAP 10",
      "equipment": [
        "barbell"
      ],
      "exercises": [
        "15 deadlifts",
        "15 hand-release push-ups"
      ],
      "createdAt": "1/25/2022, 1:15:44 PM",
      "updatedAt": "3/10/2022, 8:21:56 AM",
      "trainerTips": [
        "Deadlifts are meant to be light and fast",
        "Try to aim for unbroken sets",
        "RX Weights: 135lb/95lb"
      ]
    },
    {
      "id": "d8be2362-7b68-4ea4-a1f6-03f8bc4eede7",
      "name": "Heavy DT",
      "mode": "5 Rounds For Time",
      "equipment": [
        "barbell",
        "rope"
      ],
      "exercises": [
        "12 deadlifts",
        "9 hang power cleans",
        "6 push jerks"
      ],
      "createdAt": "11/20/2021, 5:39:07 PM",
      "updatedAt": "11/20/2021, 5:39:07 PM",
      "trainerTips": [
        "Aim for unbroken push jerks",
        "The first three rounds might feel terrible, but stick to it",
        "RX Weights: 205lb/145lb"
      ]
    }
  ]
}

Comme vous pouvez le voir, trois entraînements sont insérés. Un entraînement se compose d'un identifiant, d'un nom, d'un mode, d'un équipement, d'exercices, de createdAt, updatedAt et trainerTips.

Commençons par le plus simple et renvoyons tous les entraînements stockés et commençons par implémenter la méthode correspondante dans notre couche d'accès aux données (src/database/Workout.js).

Encore une fois, j'ai choisi de nommer la méthode à l'intérieur ici de la même manière que celle du service et du contrôleur. Mais ceci est totalement facultatif.

// In src/database/Workout.js
const DB = require("./db.json");

const getAllWorkouts = () => {
  return DB.workouts;
};

module.exports = { getAllWorkouts };

Revenez directement à notre service d'entraînement et implémentez la logique pour getAllWorkouts.

// In src/database/workoutService.js
// *** ADD ***
const Workout = require("../database/Workout");
const getAllWorkouts = () => {
  // *** ADD ***
  const allWorkouts = Workout.getAllWorkouts();
  // *** ADD ***
  return allWorkouts;
};

const getOneWorkout = () => {
  return;
};

const createNewWorkout = () => {
  return;
};

const updateOneWorkout = () => {
  return;
};

const deleteOneWorkout = () => {
  return;
};

module.exports = {
  getAllWorkouts,
  getOneWorkout,
  createNewWorkout,
  updateOneWorkout,
  deleteOneWorkout,
};

Le retour de tous les entraînements est assez simple et nous n'avons pas à faire de transformations car il s'agit déjà d'un fichier JSON. Nous n'avons pas non plus besoin d'accepter d'arguments pour l'instant. Cette mise en œuvre est donc assez simple. Mais nous y reviendrons plus tard.

De retour dans notre contrôleur d'entraînement, nous recevons la valeur de retour workoutService.getAllWorkouts()et l'envoyons simplement en réponse au client. Nous avons bouclé la réponse de la base de données via notre service vers le contrôleur.

// In src/controllers/workoutControllers.js
const workoutService = require("../services/workoutService");

const getAllWorkouts = (req, res) => {
  const allWorkouts = workoutService.getAllWorkouts();
  // *** ADD ***
  res.send({ status: "OK", data: allWorkouts });
};

const getOneWorkout = (req, res) => {
  const workout = workoutService.getOneWorkout();
  res.send("Get an existing workout");
};

const createNewWorkout = (req, res) => {
  const createdWorkout = workoutService.createNewWorkout();
  res.send("Create a new workout");
};

const updateOneWorkout = (req, res) => {
  const updatedWorkout = workoutService.updateOneWorkout();
  res.send("Update an existing workout");
};

const deleteOneWorkout = (req, res) => {
  workoutService.deleteOneWorkout();
  res.send("Delete an existing workout");
};

module.exports = {
  getAllWorkouts,
  getOneWorkout,
  createNewWorkout,
  updateOneWorkout,
  deleteOneWorkout,
};

Accédez à localhost:3000/api/v1/workouts dans votre navigateur et vous devriez voir la réponse JSON.

Capture d'écran-2022-04-30-at-11.38.14

Ça s'est super bien passé ! Nous renvoyons les données au format JSON. Mais qu'en est-il de l'accepter ? Pensons à un point de terminaison où nous devons recevoir des données JSON du client. Le point de terminaison pour la création ou la mise à jour d'un entraînement a besoin des données du client.

Dans notre contrôleur d'entraînement, nous extrayons le corps de la demande pour créer un nouvel entraînement et nous le transmettons au service d'entraînement. Dans le service d'entraînement, nous l'insérerons dans notre DB.json et renverrons l'entraînement nouvellement créé au client.

Pour pouvoir analyser le JSON envoyé dans le corps de la requête, nous devons d'abord installer body-parser et le configurer.

npm i body-parser
// In src/index.js 
const express = require("express");
// *** ADD ***
const bodyParser = require("body-parser");
const v1WorkoutRouter = require("./v1/routes/workoutRoutes");

const app = express();
const PORT = process.env.PORT || 3000;

// *** ADD ***
app.use(bodyParser.json());
app.use("/api/v1/workouts", v1WorkoutRouter);

app.listen(PORT, () => {
  console.log(`API is listening on port ${PORT}`);
});

Nous pouvons maintenant recevoir les données JSON dans nos contrôleurs sous req.body.

Afin de le tester correctement, ouvrez simplement votre client HTTP préféré (j'utilise Postman), créez une requête POST vers localhost:3000/api/v1/workouts et un corps de requête au format JSON comme ceci :

{
  "name": "Core Buster",
  "mode": "AMRAP 20",
  "equipment": [
    "rack",
    "barbell",
    "abmat"
  ],
  "exercises": [
    "15 toes to bars",
    "10 thrusters",
    "30 abmat sit-ups"
  ],
  "trainerTips": [
    "Split your toes to bars into two sets maximum",
    "Go unbroken on the thrusters",
    "Take the abmat sit-ups as a chance to normalize your breath"
  ]
}

Comme vous l'avez peut-être remarqué, il manque certaines propriétés telles que "id", "createdAt" et "updatedAt". C'est le travail de notre API d'ajouter ces propriétés avant de l'insérer. Nous nous en occuperons dans notre service d'entraînement plus tard.

Dans la méthode createNewWorkout de notre contrôleur d'entraînement, nous pouvons extraire le corps de l'objet de requête, effectuer une validation et le transmettre comme argument à notre service d'entraînement.

// In src/controllers/workoutController.js
...

const createNewWorkout = (req, res) => {
  const { body } = req;
  // *** ADD ***
  if (
    !body.name ||
    !body.mode ||
    !body.equipment ||
    !body.exercises ||
    !body.trainerTips
  ) {
    return;
  }
  // *** ADD ***
  const newWorkout = {
    name: body.name,
    mode: body.mode,
    equipment: body.equipment,
    exercises: body.exercises,
    trainerTips: body.trainerTips,
  };
  // *** ADD ***
  const createdWorkout = workoutService.createNewWorkout(newWorkout);
  // *** ADD ***
  res.status(201).send({ status: "OK", data: createdWorkout });
};

...

Pour améliorer la validation de la demande, vous utiliseriez normalement un package tiers comme express-validator .

Allons dans notre service d'entraînement et recevons les données dans notre méthode createNewWorkout.

Après cela, nous ajoutons les propriétés manquantes à l'objet et le passons à une nouvelle méthode dans notre couche d'accès aux données pour le stocker dans notre base de données.

Tout d'abord, nous créons une simple fonction Util pour écraser notre fichier JSON afin de conserver les données.

# Create a utils file inside our database directory 
touch src/database/utils.js
// In src/database/utils.js
const fs = require("fs");

const saveToDatabase = (DB) => {
  fs.writeFileSync("./src/database/db.json", JSON.stringify(DB, null, 2), {
    encoding: "utf-8",
  });
};

module.exports = { saveToDatabase };

Ensuite, nous pouvons utiliser cette fonction dans notre fichier Workout.js.

// In src/database/Workout.js
const DB = require("./db.json");
// *** ADD ***
const { saveToDatabase } = require("./utils");


const getAllWorkouts = () => {
  return DB.workouts;
};

// *** ADD ***
const createNewWorkout = (newWorkout) => {
  const isAlreadyAdded =
    DB.workouts.findIndex((workout) => workout.name === newWorkout.name) > -1;
  if (isAlreadyAdded) {
    return;
  }
  DB.workouts.push(newWorkout);
  saveToDatabase(DB);
  return newWorkout;
};

module.exports = {
  getAllWorkouts,
  // *** ADD ***
  createNewWorkout,
};

C'était lisse! L'étape suivante consiste à utiliser les méthodes de base de données de notre service d'entraînement.

# Install the uuid package 
npm i uuid
// In src/services/workoutService.js
// *** ADD ***
const { v4: uuid } = require("uuid");
// *** ADD ***
const Workout = require("../database/Workout");

const getAllWorkouts = () => {
  return DB.workouts;
};

const getOneWorkout = () => {
  return;
};

const createNewWorkout = (newWorkout) => {
  // *** ADD ***
  const workoutToInsert = {
    ...newWorkout,
    id: uuid(),
    createdAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
    updatedAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
  };
  // *** ADD ***
  const createdWorkout = Workout.createNewWorkout(workoutToInsert);
  return createdWorkout;
};

const updateOneWorkout = () => {
  return;
};

const deleteOneWorkout = () => {
  return;
};

module.exports = {
  getAllWorkouts,
  getOneWorkout,
  createNewWorkout,
  updateOneWorkout,
  deleteOneWorkout,
};

Ouah! C'était amusant, non ? Vous pouvez maintenant accéder à votre client HTTP, envoyer à nouveau la demande POST et vous devriez recevoir l'entraînement nouvellement créé au format JSON.

Si vous essayez d'ajouter le même entraînement une deuxième fois, vous recevez toujours un code d'état 201, mais sans l'entraînement nouvellement inséré.

Cela signifie que notre méthode de base de données annule l'insertion pour le moment et ne renvoie rien. C'est parce que notre instruction if pour vérifier s'il y a déjà un entraînement inséré avec le même nom entre en jeu. C'est bon pour l'instant, nous traiterons ce cas dans la prochaine meilleure pratique !

Maintenant, envoyez une requête GET à localhost:3000/api/v1/workouts pour lire tous les entraînements. Je choisis le navigateur pour cela. Vous devriez voir que notre entraînement a été inséré avec succès et a persisté :

Capture d'écran-2022-04-30-at-11.57.23

Vous pouvez implémenter les autres méthodes par vous-même ou simplement copier mes implémentations.

Tout d'abord, le contrôleur d'entraînement (vous pouvez simplement copier tout le contenu) :

// In src/controllers/workoutController.js
const workoutService = require("../services/workoutService");

const getAllWorkouts = (req, res) => {
  const allWorkouts = workoutService.getAllWorkouts();
  res.send({ status: "OK", data: allWorkouts });
};

const getOneWorkout = (req, res) => {
  const {
    params: { workoutId },
  } = req;
  if (!workoutId) {
    return;
  }
  const workout = workoutService.getOneWorkout(workoutId);
  res.send({ status: "OK", data: workout });
};

const createNewWorkout = (req, res) => {
  const { body } = req;
  if (
    !body.name ||
    !body.mode ||
    !body.equipment ||
    !body.exercises ||
    !body.trainerTips
  ) {
    return;
  }
  const newWorkout = {
    name: body.name,
    mode: body.mode,
    equipment: body.equipment,
    exercises: body.exercises,
    trainerTips: body.trainerTips,
  };
  const createdWorkout = workoutService.createNewWorkout(newWorkout);
  res.status(201).send({ status: "OK", data: createdWorkout });
};

const updateOneWorkout = (req, res) => {
  const {
    body,
    params: { workoutId },
  } = req;
  if (!workoutId) {
    return;
  }
  const updatedWorkout = workoutService.updateOneWorkout(workoutId, body);
  res.send({ status: "OK", data: updatedWorkout });
};

const deleteOneWorkout = (req, res) => {
  const {
    params: { workoutId },
  } = req;
  if (!workoutId) {
    return;
  }
  workoutService.deleteOneWorkout(workoutId);
  res.status(204).send({ status: "OK" });
};

module.exports = {
  getAllWorkouts,
  getOneWorkout,
  createNewWorkout,
  updateOneWorkout,
  deleteOneWorkout,
};

Ensuite, le service d'entraînement (vous pouvez simplement copier tout le contenu) :

// In src/services/workoutServices.js
const { v4: uuid } = require("uuid");
const Workout = require("../database/Workout");

const getAllWorkouts = () => {
  const allWorkouts = Workout.getAllWorkouts();
  return allWorkouts;
};

const getOneWorkout = (workoutId) => {
  const workout = Workout.getOneWorkout(workoutId);
  return workout;
};

const createNewWorkout = (newWorkout) => {
  const workoutToInsert = {
    ...newWorkout,
    id: uuid(),
    createdAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
    updatedAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
  };
  const createdWorkout = Workout.createNewWorkout(workoutToInsert);
  return createdWorkout;
};

const updateOneWorkout = (workoutId, changes) => {
  const updatedWorkout = Workout.updateOneWorkout(workoutId, changes);
  return updatedWorkout;
};

const deleteOneWorkout = (workoutId) => {
  Workout.deleteOneWorkout(workoutId);
};

module.exports = {
  getAllWorkouts,
  getOneWorkout,
  createNewWorkout,
  updateOneWorkout,
  deleteOneWorkout,
};

Et enfin nos méthodes de base de données à l'intérieur de la couche d'accès aux données (vous pouvez simplement copier tout le contenu) :

// In src/database/Workout.js
const DB = require("./db.json");
const { saveToDatabase } = require("./utils");

const getAllWorkouts = () => {
  return DB.workouts;
};

const getOneWorkout = (workoutId) => {
  const workout = DB.workouts.find((workout) => workout.id === workoutId);
  if (!workout) {
    return;
  }
  return workout;
};

const createNewWorkout = (newWorkout) => {
  const isAlreadyAdded =
    DB.workouts.findIndex((workout) => workout.name === newWorkout.name) > -1;
  if (isAlreadyAdded) {
    return;
  }
  DB.workouts.push(newWorkout);
  saveToDatabase(DB);
  return newWorkout;
};

const updateOneWorkout = (workoutId, changes) => {
  const indexForUpdate = DB.workouts.findIndex(
    (workout) => workout.id === workoutId
  );
  if (indexForUpdate === -1) {
    return;
  }
  const updatedWorkout = {
    ...DB.workouts[indexForUpdate],
    ...changes,
    updatedAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
  };
  DB.workouts[indexForUpdate] = updatedWorkout;
  saveToDatabase(DB);
  return updatedWorkout;
};

const deleteOneWorkout = (workoutId) => {
  const indexForDeletion = DB.workouts.findIndex(
    (workout) => workout.id === workoutId
  );
  if (indexForDeletion === -1) {
    return;
  }
  DB.workouts.splice(indexForDeletion, 1);
  saveToDatabase(DB);
};

module.exports = {
  getAllWorkouts,
  createNewWorkout,
  getOneWorkout,
  updateOneWorkout,
  deleteOneWorkout,
};

Génial! Passons à la meilleure pratique suivante et voyons comment nous pouvons gérer correctement les erreurs.

Répondre avec des codes d'erreur HTTP standard

Nous avons déjà parcouru un bon bout de chemin, mais nous n'avons pas encore terminé. Notre API a maintenant la capacité de gérer les opérations CRUD de base avec le stockage de données. C'est super, mais pas vraiment idéal.

Pourquoi? Laisse-moi expliquer.

Dans un monde parfait, tout fonctionne sans aucune erreur. Mais comme vous le savez peut-être, dans le monde réel, de nombreuses erreurs peuvent se produire, que ce soit d'un point de vue humain ou technique.

Vous connaissez probablement ce sentiment étrange lorsque les choses fonctionnent dès le début sans aucune erreur. C'est génial et agréable, mais en tant que développeurs, nous sommes plus habitués aux choses qui ne fonctionnent pas correctement. 😁

Il en va de même pour notre API. Nous devrions gérer certains cas qui pourraient mal tourner ou générer une erreur. Cela renforcera également notre API.

Lorsque quelque chose ne va pas (soit à partir de la requête, soit à l'intérieur de notre API), nous renvoyons les codes d'erreur HTTP. J'ai vu et utilisé des API qui renvoyaient tout le temps un code d'erreur 400 lorsqu'une demande était boguée sans aucun message spécifique sur POURQUOI cette erreur s'est produite ou quelle était l'erreur. Donc, le débogage est devenu une douleur.

C'est la raison pour laquelle il est toujours recommandé de renvoyer les codes d'erreur HTTP appropriés pour différents cas. Cela aide le consommateur ou l'ingénieur qui a construit l'API à identifier plus facilement le problème.

Pour améliorer l'expérience, nous pouvons également envoyer un message d'erreur rapide avec la réponse d'erreur. Mais comme je l'ai écrit dans l'introduction, ce n'est pas toujours très judicieux et devrait être considéré par l'ingénieur lui-même.

Par exemple, renvoyer quelque chose comme "Le nom d'utilisateur est déjà inscrit" doit être bien pensé car vous fournissez des informations sur vos utilisateurs que vous devriez vraiment cacher.

Dans notre API Crossfit, nous examinerons le point de terminaison de création et verrons quelles erreurs peuvent survenir et comment nous pouvons les gérer. A la fin de cette astuce vous retrouverez l'implémentation complète pour les autres endpoints.

Commençons par regarder notre méthode createNewWorkout dans notre contrôleur d'entraînement :

// In src/controllers/workoutController.js
...

const createNewWorkout = (req, res) => {
  const { body } = req;
  if (
    !body.name ||
    !body.mode ||
    !body.equipment ||
    !body.exercises ||
    !body.trainerTips
  ) {
    return;
  }
  const newWorkout = {
    name: body.name,
    mode: body.mode,
    equipment: body.equipment,
    exercises: body.exercises,
    trainerTips: body.trainerTips,
  };
  const createdWorkout = workoutService.createNewWorkout(newWorkout);
  res.status(201).send({ status: "OK", data: createdWorkout });
};

...

Nous avons déjà détecté le cas où le corps de la requête n'est pas construit correctement et nous avons des clés manquantes que nous attendons.

Ce serait un bon exemple pour renvoyer une erreur HTTP 400 avec un message d'erreur correspondant.

// In src/controllers/workoutController.js
...

const createNewWorkout = (req, res) => {
  const { body } = req;
  if (
    !body.name ||
    !body.mode ||
    !body.equipment ||
    !body.exercises ||
    !body.trainerTips
  ) {
    res
      .status(400)
      .send({
        status: "FAILED",
        data: {
          error:
            "One of the following keys is missing or is empty in request body: 'name', 'mode', 'equipment', 'exercises', 'trainerTips'",
        },
      });
    return;
  }
  const newWorkout = {
    name: body.name,
    mode: body.mode,
    equipment: body.equipment,
    exercises: body.exercises,
    trainerTips: body.trainerTips,
  };
  const createdWorkout = workoutService.createNewWorkout(newWorkout);
  res.status(201).send({ status: "OK", data: createdWorkout });
};

...

Si nous essayons d'ajouter un nouvel entraînement mais oublions de fournir la propriété "mode" dans le corps de notre requête, nous devrions voir le message d'erreur avec le code d'erreur HTTP 400.

Capture d'écran-2022-04-30-at-15.17.21

Un développeur qui utilise l'API est désormais mieux informé sur ce qu'il doit rechercher. Ils savent immédiatement qu'ils doivent entrer dans le corps de la requête et voir s'ils ont oublié de fournir l'une des propriétés requises.

Laisser ce message d'erreur plus générique pour toutes les propriétés sera acceptable pour le moment. En règle générale, vous utiliserez un validateur de schéma pour gérer cela.

Approfondissons notre service d'entraînement et voyons quelles erreurs potentielles pourraient se produire.

// In src/services/workoutService.js
...

const createNewWorkout = (newWorkout) => {
  const workoutToInsert = {
    ...newWorkout,
    id: uuid(),
    createdAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
    updatedAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
  };
  const createdWorkout = Workout.createNewWorkout(workoutToInsert);
  return createdWorkout;
};

...

Une chose qui pourrait mal tourner est l'insertion de la base de données Workout.createNewWorkout(). J'aime envelopper cette chose dans un bloc try/catch pour intercepter l'erreur lorsqu'elle se produit.

// In src/services/workoutService.js
...

const createNewWorkout = (newWorkout) => {
  const workoutToInsert = {
    ...newWorkout,
    id: uuid(),
    createdAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
    updatedAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
  };
  try {
    const createdWorkout = Workout.createNewWorkout(workoutToInsert);
    return createdWorkout;
  } catch (error) {
    throw error;
  }
};

...

Chaque erreur renvoyée dans notre méthode Workout.createNewWorkout() sera interceptée dans notre bloc catch. Nous ne faisons que le renvoyer, afin que nous puissions ajuster nos réponses plus tard dans notre contrôleur.

Définissons nos erreurs dans Workout.js :

// In src/database/Workout.js
...

const createNewWorkout = (newWorkout) => {
  const isAlreadyAdded =
    DB.workouts.findIndex((workout) => workout.name === newWorkout.name) > -1;
  if (isAlreadyAdded) {
    throw {
      status: 400,
      message: `Workout with the name '${newWorkout.name}' already exists`,
    };
  }
  try {
    DB.workouts.push(newWorkout);
    saveToDatabase(DB);
    return newWorkout;
  } catch (error) {
    throw { status: 500, message: error?.message || error };
  }
};

...

Comme vous pouvez le voir, une erreur se compose de deux choses, un statut et un message. J'utilise simplement le mot-clé throw ici pour envoyer une structure de données différente de celle d'une chaîne, ce qui est requis dans throw new Error() .

Un petit inconvénient du simple lancer est que nous n'obtenons pas de trace de pile. Mais normalement, cette erreur serait gérée par une bibliothèque tierce de notre choix (par exemple Mongoose si vous utilisez une base de données MongoDB). Mais pour les besoins de ce tutoriel, cela devrait convenir.

Nous sommes désormais en mesure de lancer et d'intercepter les erreurs dans la couche d'accès aux services et aux données. Nous pouvons maintenant passer à notre contrôleur d'entraînement, y détecter les erreurs et réagir en conséquence.

// In src/controllers/workoutController.js
...

const createNewWorkout = (req, res) => {
  const { body } = req;
  if (
    !body.name ||
    !body.mode ||
    !body.equipment ||
    !body.exercises ||
    !body.trainerTips
  ) {
    res
      .status(400)
      .send({
        status: "FAILED",
        data: {
          error:
            "One of the following keys is missing or is empty in request body: 'name', 'mode', 'equipment', 'exercises', 'trainerTips'",
        },
      });
    return;
  }
  const newWorkout = {
    name: body.name,
    mode: body.mode,
    equipment: body.equipment,
    exercises: body.exercises,
    trainerTips: body.trainerTips,
  };
  // *** ADD ***
  try {
    const createdWorkout = workoutService.createNewWorkout(newWorkout);
    res.status(201).send({ status: "OK", data: createdWorkout });
  } catch (error) {
    res
      .status(error?.status || 500)
      .send({ status: "FAILED", data: { error: error?.message || error } });
  }
};

...

Vous pouvez tester les choses en ajoutant deux fois un entraînement portant le même nom ou en ne fournissant pas une propriété requise dans le corps de votre requête. Vous devriez recevoir les codes d'erreur HTTP correspondants avec le message d'erreur.

Pour conclure et passer au conseil suivant, vous pouvez copier les autres méthodes implémentées dans les fichiers suivants ou vous pouvez l'essayer par vous-même :

// In src/controllers/workoutController.js
const workoutService = require("../services/workoutService");

const getAllWorkouts = (req, res) => {
  try {
    const allWorkouts = workoutService.getAllWorkouts();
    res.send({ status: "OK", data: allWorkouts });
  } catch (error) {
    res
      .status(error?.status || 500)
      .send({ status: "FAILED", data: { error: error?.message || error } });
  }
};

const getOneWorkout = (req, res) => {
  const {
    params: { workoutId },
  } = req;
  if (!workoutId) {
    res
      .status(400)
      .send({
        status: "FAILED",
        data: { error: "Parameter ':workoutId' can not be empty" },
      });
  }
  try {
    const workout = workoutService.getOneWorkout(workoutId);
    res.send({ status: "OK", data: workout });
  } catch (error) {
    res
      .status(error?.status || 500)
      .send({ status: "FAILED", data: { error: error?.message || error } });
  }
};

const createNewWorkout = (req, res) => {
  const { body } = req;
  if (
    !body.name ||
    !body.mode ||
    !body.equipment ||
    !body.exercises ||
    !body.trainerTips
  ) {
    res
      .status(400)
      .send({
        status: "FAILED",
        data: {
          error:
            "One of the following keys is missing or is empty in request body: 'name', 'mode', 'equipment', 'exercises', 'trainerTips'",
        },
      });
    return;
  }
  const newWorkout = {
    name: body.name,
    mode: body.mode,
    equipment: body.equipment,
    exercises: body.exercises,
    trainerTips: body.trainerTips,
  };
  try {
    const createdWorkout = workoutService.createNewWorkout(newWorkout);
    res.status(201).send({ status: "OK", data: createdWorkout });
  } catch (error) {
    res
      .status(error?.status || 500)
      .send({ status: "FAILED", data: { error: error?.message || error } });
  }
};

const updateOneWorkout = (req, res) => {
  const {
    body,
    params: { workoutId },
  } = req;
  if (!workoutId) {
    res
      .status(400)
      .send({
        status: "FAILED",
        data: { error: "Parameter ':workoutId' can not be empty" },
      });
  }
  try {
    const updatedWorkout = workoutService.updateOneWorkout(workoutId, body);
    res.send({ status: "OK", data: updatedWorkout });
  } catch (error) {
    res
      .status(error?.status || 500)
      .send({ status: "FAILED", data: { error: error?.message || error } });
  }
};

const deleteOneWorkout = (req, res) => {
  const {
    params: { workoutId },
  } = req;
  if (!workoutId) {
    res
      .status(400)
      .send({
        status: "FAILED",
        data: { error: "Parameter ':workoutId' can not be empty" },
      });
  }
  try {
    workoutService.deleteOneWorkout(workoutId);
    res.status(204).send({ status: "OK" });
  } catch (error) {
    res
      .status(error?.status || 500)
      .send({ status: "FAILED", data: { error: error?.message || error } });
  }
};

module.exports = {
  getAllWorkouts,
  getOneWorkout,
  createNewWorkout,
  updateOneWorkout,
  deleteOneWorkout,
  getRecordsForWorkout,
};
// In src/services/workoutService.js
const { v4: uuid } = require("uuid");
const Workout = require("../database/Workout");

const getAllWorkouts = () => {
  try {
    const allWorkouts = Workout.getAllWorkouts();
    return allWorkouts;
  } catch (error) {
    throw error;
  }
};

const getOneWorkout = (workoutId) => {
  try {
    const workout = Workout.getOneWorkout(workoutId);
    return workout;
  } catch (error) {
    throw error;
  }
};

const createNewWorkout = (newWorkout) => {
  const workoutToInsert = {
    ...newWorkout,
    id: uuid(),
    createdAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
    updatedAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
  };
  try {
    const createdWorkout = Workout.createNewWorkout(workoutToInsert);
    return createdWorkout;
  } catch (error) {
    throw error;
  }
};

const updateOneWorkout = (workoutId, changes) => {
  try {
    const updatedWorkout = Workout.updateOneWorkout(workoutId, changes);
    return updatedWorkout;
  } catch (error) {
    throw error;
  }
};

const deleteOneWorkout = (workoutId) => {
  try {
    Workout.deleteOneWorkout(workoutId);
  } catch (error) {
    throw error;
  }
};

module.exports = {
  getAllWorkouts,
  getOneWorkout,
  createNewWorkout,
  updateOneWorkout,
  deleteOneWorkout,
};
// In src/database/Workout.js
const DB = require("./db.json");
const { saveToDatabase } = require("./utils");

const getAllWorkouts = () => {
  try {
    return DB.workouts;
  } catch (error) {
    throw { status: 500, message: error };
  }
};

const getOneWorkout = (workoutId) => {
  try {
    const workout = DB.workouts.find((workout) => workout.id === workoutId);
    if (!workout) {
      throw {
        status: 400,
        message: `Can't find workout with the id '${workoutId}'`,
      };
    }
    return workout;
  } catch (error) {
    throw { status: error?.status || 500, message: error?.message || error };
  }
};

const createNewWorkout = (newWorkout) => {
  try {
    const isAlreadyAdded =
      DB.workouts.findIndex((workout) => workout.name === newWorkout.name) > -1;
    if (isAlreadyAdded) {
      throw {
        status: 400,
        message: `Workout with the name '${newWorkout.name}' already exists`,
      };
    }
    DB.workouts.push(newWorkout);
    saveToDatabase(DB);
    return newWorkout;
  } catch (error) {
    throw { status: error?.status || 500, message: error?.message || error };
  }
};

const updateOneWorkout = (workoutId, changes) => {
  try {
    const isAlreadyAdded =
      DB.workouts.findIndex((workout) => workout.name === changes.name) > -1;
    if (isAlreadyAdded) {
      throw {
        status: 400,
        message: `Workout with the name '${changes.name}' already exists`,
      };
    }
    const indexForUpdate = DB.workouts.findIndex(
      (workout) => workout.id === workoutId
    );
    if (indexForUpdate === -1) {
      throw {
        status: 400,
        message: `Can't find workout with the id '${workoutId}'`,
      };
    }
    const updatedWorkout = {
      ...DB.workouts[indexForUpdate],
      ...changes,
      updatedAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
    };
    DB.workouts[indexForUpdate] = updatedWorkout;
    saveToDatabase(DB);
    return updatedWorkout;
  } catch (error) {
    throw { status: error?.status || 500, message: error?.message || error };
  }
};

const deleteOneWorkout = (workoutId) => {
  try {
    const indexForDeletion = DB.workouts.findIndex(
      (workout) => workout.id === workoutId
    );
    if (indexForDeletion === -1) {
      throw {
        status: 400,
        message: `Can't find workout with the id '${workoutId}'`,
      };
    }
    DB.workouts.splice(indexForDeletion, 1);
    saveToDatabase(DB);
  } catch (error) {
    throw { status: error?.status || 500, message: error?.message || error };
  }
};

module.exports = {
  getAllWorkouts,
  createNewWorkout,
  getOneWorkout,
  updateOneWorkout,
  deleteOneWorkout,
};

Évitez les verbes dans les noms de point de terminaison

Cela n'a pas beaucoup de sens d'utiliser des verbes à l'intérieur de vos points de terminaison et est, en fait, assez inutile. Généralement, chaque URL doit pointer vers une ressource (rappelez-vous l'exemple de la boîte ci-dessus). Rien de plus et rien de moins.

L'utilisation d'un verbe à l'intérieur d'une URL montre un certain comportement qu'une ressource elle-même ne peut pas avoir.

Nous avons déjà correctement implémenté les points de terminaison sans utiliser de verbes à l'intérieur de l'URL, mais regardons à quoi ressembleraient nos URL si nous avions utilisé des verbes.

// Current implementations (without verbs)
GET "/api/v1/workouts" 
GET "/api/v1/workouts/:workoutId" 
POST "/api/v1/workouts" 
PATCH "/api/v1/workouts/:workoutId" 
DELETE "/api/v1/workouts/:workoutId"  

// Implementation using verbs 
GET "/api/v1/getAllWorkouts" 
GET "/api/v1/getWorkoutById/:workoutId" 
CREATE "/api/v1/createWorkout" 
PATCH "/api/v1/updateWorkout/:workoutId" 
DELETE "/api/v1/deleteWorkout/:workoutId"

Voyez-vous la différence? Avoir une URL complètement différente pour chaque comportement peut rapidement devenir déroutant et inutilement complexe.

Imaginez que nous ayons 300 terminaux différents. L'utilisation d'une URL distincte pour chacun peut être un enfer de surcharge (et de documentation).

Une autre raison que j'aimerais souligner pour ne pas utiliser de verbes dans votre URL est que le verbe HTTP lui-même indique déjà l'action.

Des choses comme "GET /api/v1/getAllWorkouts" ou "DELETE api/v1/deleteWorkout/workoutId" sont inutiles.

Lorsque vous jetez un coup d'œil à notre implémentation actuelle, elle devient beaucoup plus propre car nous n'utilisons que deux URL différentes et le comportement réel est géré via le verbe HTTP et la charge utile de la requête correspondante.

J'imagine toujours que le verbe HTTP décrit l'action (ce que nous aimerions faire) et l'URL elle-même (qui pointe vers une ressource) la cible. "GET /api/v1/workouts" est également plus fluide en langage humain.

Regrouper les ressources associées (imbrication logique)

Lorsque vous concevez votre API, il peut y avoir des cas où vous avez des ressources associées à d'autres. Il est recommandé de les regrouper en un point de terminaison et de les imbriquer correctement.

Considérons que, dans notre API, nous avons également une liste de membres qui sont inscrits dans notre boîte CrossFit ("boîte" est le nom d'une salle de sport CrossFit). Afin de motiver nos membres, nous suivons les records de box globaux pour chaque entraînement.

Par exemple, il y a une séance d'entraînement où vous devez faire un certain ordre d'exercices le plus rapidement possible. Nous enregistrons les temps pour tous les membres afin d'avoir une liste du temps pour chaque membre qui a terminé cet entraînement.

Désormais, l'interface a besoin d'un point de terminaison qui répond avec tous les enregistrements d'un entraînement spécifique afin de l'afficher dans l'interface utilisateur.

Les entraînements, les membres et les enregistrements sont stockés à différents endroits de la base de données. Donc, ce dont nous avons besoin ici, c'est d'une boîte (disques) à l'intérieur d'une autre boîte (entraînements), n'est-ce pas ?

L'URI de ce point de terminaison sera /api/v1/workouts/:workoutId/records . C'est une bonne pratique pour permettre l'imbrication logique des URL. L'URL elle-même ne doit pas nécessairement refléter la structure de la base de données.

Commençons à implémenter ce point de terminaison.

Tout d'abord, ajoutez une nouvelle table dans votre db.json appelée "members". Placez-le sous "entraînements".

{
  "workouts": [ ...
  ],
  "members": [
    {
      "id": "12a410bc-849f-4e7e-bfc8-4ef283ee4b19",
      "name": "Jason Miller",
      "gender": "male",
      "dateOfBirth": "23/04/1990",
      "email": "jason@mail.com",
      "password": "666349420ec497c1dc890c45179d44fb13220239325172af02d1fb6635922956"
    },
    {
      "id": "2b9130d4-47a7-4085-800e-0144f6a46059",
      "name": "Tiffany Brookston",
      "gender": "female",
      "dateOfBirth": "09/06/1996",
      "email": "tiffy@mail.com",
      "password": "8a1ea5669b749354110dcba3fac5546c16e6d0f73a37f35a84f6b0d7b3c22fcc"
    },
    {
      "id": "11817fb1-03a1-4b4a-8d27-854ac893cf41",
      "name": "Catrin Stevenson",
      "gender": "female",
      "dateOfBirth": "17/08/2001",
      "email": "catrin@mail.com",
      "password": "18eb2d6c5373c94c6d5d707650d02c3c06f33fac557c9cfb8cb1ee625a649ff3"
    },
    {
      "id": "6a89217b-7c28-4219-bd7f-af119c314159",
      "name": "Greg Bronson",
      "gender": "male",
      "dateOfBirth": "08/04/1993",
      "email": "greg@mail.com",
      "password": "a6dcde7eceb689142f21a1e30b5fdb868ec4cd25d5537d67ac7e8c7816b0e862"
    }
  ]
}

Avant de commencer à demander - oui, les mots de passe sont hachés. 😉

Après cela, ajoutez quelques "enregistrements" sous "membres".

{
  "workouts": [ ...
  ],
  "members": [ ...
  ],
  "records": [
    {
      "id": "ad75d475-ac57-44f4-a02a-8f6def58ff56",
      "workout": "4a3d9aaa-608c-49a7-a004-66305ad4ab50",
      "record": "160 reps"
    },
    {
      "id": "0bff586f-2017-4526-9e52-fe3ea46d55ab",
      "workout": "d8be2362-7b68-4ea4-a1f6-03f8bc4eede7",
      "record": "7:23 minutes"
    },
    {
      "id": "365cc0bb-ba8f-41d3-bf82-83d041d38b82",
      "workout": "a24d2618-01d1-4682-9288-8de1343e53c7",
      "record": "358 reps"
    },
    {
      "id": "62251cfe-fdb6-4fa6-9a2d-c21be93ac78d",
      "workout": "4a3d9aaa-608c-49a7-a004-66305ad4ab50",
      "record": "145 reps"
    }
  ],
}

Pour vous assurer que vous avez les mêmes entraînements que moi avec les mêmes identifiants, copiez également les entraînements :

{
  "workouts": [
    {
      "id": "61dbae02-c147-4e28-863c-db7bd402b2d6",
      "name": "Tommy V",
      "mode": "For Time",
      "equipment": [
        "barbell",
        "rope"
      ],
      "exercises": [
        "21 thrusters",
        "12 rope climbs, 15 ft",
        "15 thrusters",
        "9 rope climbs, 15 ft",
        "9 thrusters",
        "6 rope climbs, 15 ft"
      ],
      "createdAt": "4/20/2022, 2:21:56 PM",
      "updatedAt": "4/20/2022, 2:21:56 PM",
      "trainerTips": [
        "Split the 21 thrusters as needed",
        "Try to do the 9 and 6 thrusters unbroken",
        "RX Weights: 115lb/75lb"
      ]
    },
    {
      "id": "4a3d9aaa-608c-49a7-a004-66305ad4ab50",
      "name": "Dead Push-Ups",
      "mode": "AMRAP 10",
      "equipment": [
        "barbell"
      ],
      "exercises": [
        "15 deadlifts",
        "15 hand-release push-ups"
      ],
      "createdAt": "1/25/2022, 1:15:44 PM",
      "updatedAt": "3/10/2022, 8:21:56 AM",
      "trainerTips": [
        "Deadlifts are meant to be light and fast",
        "Try to aim for unbroken sets",
        "RX Weights: 135lb/95lb"
      ]
    },
    {
      "id": "d8be2362-7b68-4ea4-a1f6-03f8bc4eede7",
      "name": "Heavy DT",
      "mode": "5 Rounds For Time",
      "equipment": [
        "barbell",
        "rope"
      ],
      "exercises": [
        "12 deadlifts",
        "9 hang power cleans",
        "6 push jerks"
      ],
      "createdAt": "11/20/2021, 5:39:07 PM",
      "updatedAt": "4/22/2022, 5:49:18 PM",
      "trainerTips": [
        "Aim for unbroken push jerks",
        "The first three rounds might feel terrible, but stick to it",
        "RX Weights: 205lb/145lb"
      ]
    },
    {
      "name": "Core Buster",
      "mode": "AMRAP 20",
      "equipment": [
        "rack",
        "barbell",
        "abmat"
      ],
      "exercises": [
        "15 toes to bars",
        "10 thrusters",
        "30 abmat sit-ups"
      ],
      "trainerTips": [
        "Split your toes to bars in two sets maximum",
        "Go unbroken on the thrusters",
        "Take the abmat sit-ups as a chance to normalize your breath"
      ],
      "id": "a24d2618-01d1-4682-9288-8de1343e53c7",
      "createdAt": "4/22/2022, 5:50:17 PM",
      "updatedAt": "4/22/2022, 5:50:17 PM"
    }
  ],
  "members": [ ...
  ],
  "records": [ ...
  ]
}

Bon, prenons quelques minutes pour réfléchir à notre implémentation.

Nous avons une ressource appelée "workouts" d'un côté et une autre appelée "records" de l'autre côté.

Pour avancer dans notre architecture, il serait conseillé de créer un autre contrôleur, un autre service et une autre collection de méthodes de base de données responsables des enregistrements.

Il y a de fortes chances que nous implémentions également des points de terminaison CRUD pour les enregistrements, car les enregistrements devraient également être ajoutés, mis à jour ou supprimés à l'avenir. Mais ce ne sera pas la tâche principale pour l'instant.

Nous aurons également besoin d'un routeur d'enregistrements pour intercepter les demandes spécifiques d'enregistrements, mais nous n'en avons pas besoin pour le moment. Cela pourrait être une excellente occasion pour vous de mettre en œuvre les opérations CRUD pour les enregistrements avec leurs propres itinéraires et de vous entraîner un peu.

# Create records controller 
touch src/controllers/recordController.js 

# Create records service 
touch src/services/recordService.js 

# Create records database methods 
touch src/database/Record.js

C'était facile. Passons à autre chose et commençons en arrière avec la mise en œuvre de nos méthodes de base de données.

// In src/database/Record.js
const DB = require("./db.json");

const getRecordForWorkout = (workoutId) => {
  try {
    const record = DB.records.filter((record) => record.workout === workoutId);
    if (!record) {
      throw {
        status: 400,
        message: `Can't find workout with the id '${workoutId}'`,
      };
    }
    return record;
  } catch (error) {
    throw { status: error?.status || 500, message: error?.message || error };
  }
};
module.exports = { getRecordForWorkout };

Assez simple, non? Nous filtrons tous les enregistrements liés à l'ID d'entraînement hors du paramètre de requête.

Le suivant est notre service d'enregistrement :

// In src/services/recordService.js
const Record = require("../database/Record");

const getRecordForWorkout = (workoutId) => {
  try {
    const record = Record.getRecordForWorkout(workoutId);
    return record;
  } catch (error) {
    throw error;
  }
};
module.exports = { getRecordForWorkout };

Encore une fois, rien de nouveau ici.

Nous pouvons maintenant créer un nouvel itinéraire dans notre routeur d'entraînement et diriger la demande vers notre service d'enregistrement.

// In src/v1/routes/workoutRoutes.js
const express = require("express");
const workoutController = require("../../controllers/workoutController");
// *** ADD ***
const recordController = require("../../controllers/recordController");

const router = express.Router();

router.get("/", workoutController.getAllWorkouts);

router.get("/:workoutId", workoutController.getOneWorkout);

// *** ADD ***
router.get("/:workoutId/records", recordController.getRecordForWorkout);

router.post("/", workoutController.createNewWorkout);

router.patch("/:workoutId", workoutController.updateOneWorkout);

router.delete("/:workoutId", workoutController.deleteOneWorkout);

module.exports = router;

Génial! Testons les choses dans notre navigateur.

Tout d'abord, nous récupérons tous les entraînements pour obtenir un identifiant d'entraînement.

Capture d'écran-2022-04-30-at-15.36.48

Voyons si nous pouvons récupérer tous les enregistrements pour cela :

Capture d'écran-2022-04-30-at-15.36.32

Comme vous pouvez le voir, l'imbrication logique a du sens lorsque vous avez des ressources qui peuvent être liées ensemble. Théoriquement, vous pouvez l'imbriquer à la profondeur que vous voulez, mais la règle d'or ici est d'aller trois niveaux de profondeur au maximum.

Si vous souhaitez imbriquer plus profondément que cela, vous pouvez faire un petit ajustement dans vos enregistrements de base de données. Je vais vous montrer un petit exemple.

Imaginez que le frontend ait également besoin d'un point de terminaison pour obtenir des informations sur le membre qui détient exactement l'enregistrement actuel et souhaite recevoir des métadonnées à son sujet.

Bien sûr, nous pourrions implémenter l'URI suivante :

GET /api/v1/workouts/:workoutId/records/members/:memberId

Le point de terminaison devient maintenant moins gérable à mesure que nous y ajoutons de l'imbrication. Par conséquent, il est recommandé de stocker l'URI pour recevoir des informations sur un membre directement dans l'enregistrement.

Tenez compte des éléments suivants dans la base de données :

{
  "workouts": [ ...
  ],
  "members": [ ...
  ],
  "records": [ ... {
      "id": "ad75d475-ac57-44f4-a02a-8f6def58ff56",
      "workout": "4a3d9aaa-608c-49a7-a004-66305ad4ab50",
      "record": "160 reps",
      "memberId": "11817fb1-03a1-4b4a-8d27-854ac893cf41",
      "member": "/members/:memberId"
    },
  ]
}

Comme vous pouvez le voir, nous avons ajouté les deux propriétés "memberId" et "member" à nos enregistrements dans la base de données. Cela a l'énorme avantage que nous n'avons pas à imbriquer plus profondément notre point de terminaison existant.

L'interface a juste besoin d'appeler GET /api/v1/workouts/:workoutId/records et reçoit automatiquement tous les enregistrements liés à cet entraînement.

En plus de cela, il obtient l'identifiant du membre et le point de terminaison pour récupérer des informations sur ce membre. Ainsi, nous avons évité l'imbrication plus profonde de notre point de terminaison.

Bien sûr, cela ne fonctionne que si nous pouvons gérer les demandes adressées à "/members/:memberId" 😁 Cela semble être une excellente opportunité de formation pour vous permettre de mettre en œuvre cette situation !

Intégrez le filtrage, le tri et la pagination

À l'heure actuelle, nous sommes en mesure d'effectuer un certain nombre d'opérations avec notre API. C'est un grand progrès, mais il y a plus.

Au cours des dernières sections, nous nous sommes concentrés sur l'amélioration de notre expérience de développeur et sur la manière dont notre API peut interagir avec. Mais la performance globale de notre API est un autre facteur clé sur lequel nous devons travailler.

C'est pourquoi l'intégration du filtrage, du tri et de la pagination est également un facteur essentiel sur ma liste.

Imaginez que nous ayons 2 000 entraînements, 450 enregistrements et 500 membres stockés dans notre base de données. Lorsque nous appelons notre point de terminaison pour obtenir tous les entraînements, nous ne voulons pas envoyer les 2 000 entraînements à la fois. Ce sera une réponse très lente bien sûr, ou cela fera tomber nos systèmes (peut-être avec 200 000 😁).

C'est la raison pour laquelle le filtrage et la pagination sont importants. Le filtrage, comme son nom l'indique déjà, est utile car il nous permet d'extraire des données spécifiques de l'ensemble de notre collection. Par exemple, tous les entraînements qui ont le mode "For Time".

La pagination est un autre mécanisme pour diviser toute notre collection d'entraînements en plusieurs "pages" où chaque page ne comprend que vingt entraînements, par exemple. Cette technique nous aide à nous assurer que nous n'envoyons pas plus de vingt entraînements en même temps avec notre réponse au client.

Le tri peut être une tâche complexe. Il est donc plus efficace de le faire dans notre API et d'envoyer les données triées au client.

Commençons par intégrer un mécanisme de filtrage dans notre API. Nous mettrons à jour notre point de terminaison qui envoie tous les entraînements en acceptant les paramètres de filtre. Normalement, dans une requête GET, nous ajoutons les critères de filtre en tant que paramètre de requête.

Notre nouvel URI ressemblera à ceci, lorsque nous aimerions obtenir uniquement les entraînements qui sont en mode "AMRAP" ( A s M any R ounds A s P ossible ): /api/v1/workouts?mode=amrap .

Pour rendre cela plus amusant, nous devons ajouter quelques séances d'entraînement supplémentaires. Collez ces entraînements dans votre collection "entraînements" à l'intérieur de db.json :

{
  "name": "Jumping (Not) Made Easy",
  "mode": "AMRAP 12",
  "equipment": [
    "jump rope"
  ],
  "exercises": [
    "10 burpees",
    "25 double-unders"
  ],
  "trainerTips": [
    "Scale to do 50 single-unders, if double-unders are too difficult"
  ],
  "id": "8f8318f8-b869-4e9d-bb78-88010193563a",
  "createdAt": "4/25/2022, 2:45:28 PM",
  "updatedAt": "4/25/2022, 2:45:28 PM"
},
{
  "name": "Burpee Meters",
  "mode": "3 Rounds For Time",
  "equipment": [
    "Row Erg"
  ],
  "exercises": [
    "Row 500 meters",
    "21 burpees",
    "Run 400 meters",
    "Rest 3 minutes"
  ],
  "trainerTips": [
    "Go hard",
    "Note your time after the first run",
    "Try to hold your pace"
  ],
  "id": "0a5948af-5185-4266-8c4b-818889657e9d",
  "createdAt": "4/25/2022, 2:48:53 PM",
  "updatedAt": "4/25/2022, 2:48:53 PM"
},
{
  "name": "Dumbbell Rower",
  "mode": "AMRAP 15",
  "equipment": [
    "Dumbbell"
  ],
  "exercises": [
    "15 dumbbell rows, left arm",
    "15 dumbbell rows, right arm",
    "50-ft handstand walk"
  ],
  "trainerTips": [
    "RX weights for women: 35-lb",
    "RX weights for men: 50-lb"
  ],
  "id": "3dc53bc8-27b8-4773-b85d-89f0a354d437",
  "createdAt": "4/25/2022, 2:56:03 PM",
  "updatedAt": "4/25/2022, 2:56:03 PM"
}

Après cela, nous devons accepter et gérer les paramètres de requête. Notre contrôleur d'entraînement sera le bon endroit pour commencer :

// In src/controllers/workoutController.js
...

const getAllWorkouts = (req, res) => {
  // *** ADD ***
  const { mode } = req.query;
  try {
    // *** ADD ***
    const allWorkouts = workoutService.getAllWorkouts({ mode });
    res.send({ status: "OK", data: allWorkouts });
  } catch (error) {
    res
      .status(error?.status || 500)
      .send({ status: "FAILED", data: { error: error?.message || error } });
  }
};

...

Nous extrayons "mode" de l'objet req.query et définissons un paramètre de workoutService.getAllWorkouts. Ce sera un objet composé de nos paramètres de filtre.

J'utilise ici la syntaxe abrégée pour créer une nouvelle clé appelée "mode" à l'intérieur de l'objet avec la valeur de tout ce qui se trouve dans "req.query.mode". Il peut s'agir d'une valeur véridique ou indéfinie s'il n'y a pas de paramètre de requête appelé "mode". Nous pouvons étendre cet objet au plus de paramètres de filtre que nous aimerions accepter.

Dans notre service d'entraînement, transmettez-le à votre méthode de base de données :

// In src/services/workoutService.js
...

const getAllWorkouts = (filterParams) => {
  try {
    // *** ADD ***
    const allWorkouts = Workout.getAllWorkouts(filterParams);
    return allWorkouts;
  } catch (error) {
    throw error;
  }
};

...

Nous pouvons maintenant l'utiliser dans notre méthode de base de données et appliquer le filtrage :

// In src/database/Workout.js
...

const getAllWorkouts = (filterParams) => {
  try {
    let workouts = DB.workouts;
    if (filterParams.mode) {
      return DB.workouts.filter((workout) =>
        workout.mode.toLowerCase().includes(filterParams.mode)
      );
    }
    // Other if-statements will go here for different parameters
    return workouts;
  } catch (error) {
    throw { status: 500, message: error };
  }
};

...

Assez simple, non? Tout ce que nous faisons ici est de vérifier si nous avons réellement une valeur véridique pour la clé "mode" dans nos "filterParams". Si cela est vrai, nous filtrons tous les entraînements qui ont le même "mode". Si ce n'est pas vrai, alors il n'y a pas de paramètre de requête appelé "mode" et nous renvoyons tous les entraînements car nous n'avons pas besoin de filtrer.

Nous avons défini ici les "entraînements" comme une variable "let" car lors de l'ajout d'instructions if pour différents filtres, nous pouvons écraser les "entraînements" et enchaîner les filtres.

Dans votre navigateur, vous pouvez visiter localhost:3000/api/v1/workouts?mode=amrap et vous recevrez tous les entraînements "AMRAP" qui sont stockés :

Capture d'écran-2022-04-30-at-15.48.57

Si vous laissez le paramètre de requête de côté, vous devriez obtenir tous les entraînements comme avant. Vous pouvez essayer plus loin en ajoutant "for%20time" comme valeur pour le paramètre "mode" (rappelez-vous --> "%20" signifie "whitespace") et vous devriez recevoir tous les entraînements qui ont le mode "For Time" si il y en a de stocké.

Lorsque vous tapez une valeur qui n'est pas stockée, vous devriez recevoir un tableau vide.

Les paramètres de tri et de pagination suivent la même philosophie. Examinons quelques fonctionnalités que nous pourrions éventuellement implémenter :

  • Recevez tous les entraînements nécessitant une barre : /api/v1/workouts?equipment=barbell
  • Obtenez seulement 5 entraînements : /api/v1/workouts?length=5
  • Lorsque vous utilisez la pagination, recevez la deuxième page : /api/v1/workouts?page=2
  • Triez les entraînements dans la réponse par ordre décroissant en fonction de leur date de création : /api/v1/workouts?sort=-createdAt
  • Vous pouvez également combiner les paramètres, pour obtenir les 10 derniers entraînements mis à jour par exemple : /api/v1/workouts?sort=-updatedAt&length=10

Utiliser la mise en cache des données pour améliorer les performances

L'utilisation d'un cache de données est également une excellente pratique pour améliorer l'expérience globale et les performances de notre API.

Il est très logique d'utiliser un cache pour servir les données, lorsque les données sont une ressource souvent demandée ou/et que l'interrogation des données de la base de données est lourde et peut prendre plusieurs secondes.

Vous pouvez stocker ce type de données dans votre cache et les servir à partir de là au lieu d'aller à la base de données à chaque fois pour interroger les données.

Une chose importante que vous devez garder à l'esprit lorsque vous servez des données à partir d'un cache est que ces données peuvent devenir obsolètes. Vous devez donc vous assurer que les données à l'intérieur du cache sont toujours à jour.

Il existe de nombreuses solutions différentes. Un exemple approprié consiste à utiliser redis ou le middleware express apicache .

J'aimerais aller avec apicache, mais si vous voulez utiliser Redis, je peux fortement vous recommander de consulter leurs excellents docs .

Réfléchissons une seconde à un scénario dans notre API où un cache aurait du sens. Je pense que demander de recevoir toutes les séances d'entraînement serait effectivement servi à partir de notre cache.

Tout d'abord, installons notre middleware :

npm i apicache

Maintenant, nous devons l'importer dans notre routeur d'entraînement et le configurer.

// In src/v1/routes/workoutRoutes.js
const express = require("express");
// *** ADD ***
const apicache = require("apicache");
const workoutController = require("../../controllers/workoutController");
const recordController = require("../../controllers/recordController");

const router = express.Router();
// *** ADD ***
const cache = apicache.middleware;

// *** ADD ***
router.get("/", cache("2 minutes"), workoutController.getAllWorkouts);

router.get("/:workoutId", workoutController.getOneWorkout);

router.get("/:workoutId/records", recordController.getRecordForWorkout);

router.post("/", workoutController.createNewWorkout);

router.patch("/:workoutId", workoutController.updateOneWorkout);

router.delete("/:workoutId", workoutController.deleteOneWorkout);

module.exports = router;

La mise en route est assez simple, non ? Nous pouvons définir un nouveau cache en appelant apicache.middleware et l'utiliser comme middleware dans notre route get. Vous n'avez qu'à le mettre comme paramètre entre le chemin réel et notre contrôleur d'entraînement.

À l'intérieur, vous pouvez définir combien de temps vos données doivent être mises en cache. Pour les besoins de ce tutoriel, j'ai choisi deux minutes. Le temps dépend de la vitesse ou de la fréquence à laquelle vos données dans votre cache changent.

Testons les choses !

Dans Postman ou un autre client HTTP de votre choix, définissez une nouvelle requête qui obtient tous les entraînements. Je l'ai fait dans le navigateur jusqu'à présent, mais j'aimerais mieux visualiser les temps de réponse pour vous. C'est la raison pour laquelle je demande la ressource via Postman en ce moment.

Appelons notre requête pour la première fois :

Capture d'écran-2022-04-26-at-15.36.46-1

Comme vous pouvez le voir, il a fallu 22,93 ms à notre API pour répondre. Une fois que notre cache est à nouveau vide (au bout de deux minutes), il faut la remplir à nouveau. Cela se produit avec notre première demande.

Ainsi, dans le cas ci-dessus, les données n'ont PAS été servies à partir de notre cache. Il a pris le chemin "normal" de la base de données et a rempli notre cache.

Maintenant, avec notre deuxième requête, nous recevons un temps de réponse plus court, car elle a été directement servie depuis le cache :

Capture d'écran-2022-04-26-at-15.36.59-1

Nous avons pu servir trois fois plus vite que lors de notre précédente demande ! Tout cela grâce à notre cache.

Dans notre exemple, nous n'avons mis en cache qu'une seule route, mais vous pouvez également mettre en cache toutes les routes en l'implémentant comme ceci :

// In src/index.js
const express = require("express");
const bodyParser = require("body-parser");
// *** ADD ***
const apicache = require("apicache");
const v1WorkoutRouter = require("./v1/routes/workoutRoutes");

const app = express();
// *** ADD ***
const cache = apicache.middleware;
const PORT = process.env.PORT || 3000;

app.use(bodyParser.json());
// *** ADD ***
app.use(cache("2 minutes"));
app.use("/api/v1/workouts", v1WorkoutRouter);

app.listen(PORT, () => {
  console.log(`API is listening on port ${PORT}`);
});

Il y a une chose importante que je voudrais noter ici en ce qui concerne la mise en cache. Bien qu'il semble résoudre beaucoup de problèmes pour vous, cela peut également entraîner des problèmes dans votre application.

Quelques éléments à prendre en compte lors de l'utilisation d'un cache :

  • vous devez toujours vous assurer que les données à l'intérieur du cache sont à jour car vous ne voulez pas servir de données obsolètes
  • pendant que la première requête est en cours de traitement et que le cache est sur le point d'être rempli et que d'autres requêtes arrivent, vous devez décider si vous retardez ces autres requêtes et servez les données du cache ou si elles reçoivent également des données directement de la base de données comme la première demande
  • c'est un autre composant à l'intérieur de votre infrastructure si vous choisissez un cache distribué comme Redis (vous devez donc vous demander s'il est vraiment logique de l'utiliser)

Voici comment procéder généralement :

J'aime commencer aussi simplement et proprement que possible avec tout ce que je construis. Il en va de même pour les API.

Lorsque je commence à créer une API et qu'il n'y a aucune raison particulière d'utiliser immédiatement un cache, je le laisse de côté et je vois ce qui se passe au fil du temps. Lorsque des raisons se présentent d'utiliser un cache, je peux alors l'implémenter.

Bonnes pratiques de sécurité

Ouah! Cela a été un très bon voyage jusqu'à présent. Nous avons abordé de nombreux points importants et étendu notre API en conséquence.

Nous avons parlé des meilleures pratiques pour augmenter la convivialité et les performances de notre API. La sécurité est également un facteur clé pour les API. Vous pouvez créer la meilleure API, mais lorsqu'il s'agit d'un logiciel vulnérable exécuté sur un serveur, il devient inutile et dangereux.

Le premier et absolu must have est d'utiliser SSL/TLS car c'est un standard de nos jours pour les communications sur internet. C'est encore plus important pour les API où des données privées sont envoyées entre le client et notre API.

Si vous avez des ressources qui ne devraient être disponibles que pour les utilisateurs authentifiés, vous devez les protéger avec une vérification d'authentification.

Dans Express, par exemple, vous pouvez l'implémenter en tant que middleware comme nous l'avons fait avec notre cache pour des routes spécifiques et vérifier d'abord si la requête est authentifiée avant d'accéder à une ressource.

Il peut également y avoir des ressources ou des interactions avec notre API que nous ne voulons pas permettre à tous les utilisateurs de demander. Ensuite, vous devez proposer un système de rôles pour vos utilisateurs. Vous devez donc ajouter une autre logique de vérification à cette route et valider si l'utilisateur a le privilège d'accéder à cette ressource.

Les rôles d'utilisateur auraient également du sens dans notre cas d'utilisation lorsque nous voulons que seuls des utilisateurs spécifiques (comme des entraîneurs) créent, mettent à jour et suppriment nos entraînements et enregistrements. La lecture peut être pour tout le monde (également les membres "réguliers").

Cela peut être géré dans un autre middleware que nous utilisons pour les routes que nous souhaitons protéger. Par exemple, notre requête POST à ​​/api/v1/workouts pour créer un nouvel entraînement.

Dans le premier middleware, nous vérifierons si l'utilisateur est authentifié. Si c'est vrai, nous passerons au middleware suivant, celui qui vérifiera le rôle de l'utilisateur. Si l'utilisateur a le rôle approprié pour accéder à cette ressource, la demande est transmise au contrôleur correspondant.

À l'intérieur du gestionnaire de route, cela ressemblerait à ceci :

// In src/v1/routes/workoutRoutes.js
...

// Custom made middlewares
const authenticate = require("../../middlewares/authenticate");
const authorize = require("../../middlewares/authorize");

router.post("/", authenticate, authorize, workoutController.createNewWorkout);

...

Pour en savoir plus et obtenir d'autres bonnes pratiques sur ce sujet, je vous suggère de lire cet article .

Documentez correctement votre API

Je sais que la documentation n'est certainement pas une tâche préférée des développeurs, mais c'est une chose nécessaire à faire. Surtout quand il s'agit d'une API.

Certaines personnes disent:

"Une API est aussi bonne que sa documentation"

Je pense qu'il y a beaucoup de vérité dans cette affirmation car si une API n'est pas bien documentée, elle ne peut pas être utilisée correctement et devient donc inutile. La documentation aide également à faciliter la vie des développeurs.

Rappelez-vous toujours que la documentation est généralement la première interaction des consommateurs avec votre API. Plus les utilisateurs peuvent comprendre la documentation rapidement, plus ils peuvent utiliser l'API rapidement.

Donc, c'est à nous de mettre en place une documentation bonne et précise. Il existe d'excellents outils qui nous facilitent la vie.

Comme dans d'autres domaines de l'informatique, il existe également une sorte de norme pour documenter les API appelée OpenAPI Specification .

Voyons comment nous pouvons créer une documentation qui justifie cette spécification. Nous utiliserons les packages swagger-ui-express et swagger-jsdoc pour y parvenir. Vous serez étonné de voir à quel point c'est génial en une seconde !

Tout d'abord, nous configurons notre structure nue pour notre documentation. Parce que nous avons prévu d'avoir différentes versions de notre API, les documents seront également un peu différents. C'est la raison pour laquelle j'aimerais définir notre fichier swagger pour faire tourner notre documentation dans le dossier de version correspondant.

# Install required npm packages 
npm i swagger-jsdoc swagger-ui-express 

# Create a new file to setup the swagger docs 
touch src/v1/swagger.js
// In src/v1/swagger.js
const swaggerJSDoc = require("swagger-jsdoc");
const swaggerUi = require("swagger-ui-express");

// Basic Meta Informations about our API
const options = {
  definition: {
    openapi: "3.0.0",
    info: { title: "Crossfit WOD API", version: "1.0.0" },
  },
  apis: ["./src/v1/routes/workoutRoutes.js", "./src/database/Workout.js"],
};

// Docs in JSON format
const swaggerSpec = swaggerJSDoc(options);

// Function to setup our docs
const swaggerDocs = (app, port) => {
  // Route-Handler to visit our docs
  app.use("/api/v1/docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec));
  // Make our docs in JSON format available
  app.get("/api/v1/docs.json", (req, res) => {
    res.setHeader("Content-Type", "application/json");
    res.send(swaggerSpec);
  });
  console.log(
    `Version 1 Docs are available on http://localhost:${port}/api/v1/docs`
  );
};

module.exports = { swaggerDocs };

Ainsi, la configuration est assez simple. Nous avons défini certaines métadonnées de base de notre API, créé les documents au format JSON et créé une fonction qui rend nos documents disponibles.

Pour contrôler si tout est opérationnel, nous enregistrons un simple message sur la console où nous pouvons trouver nos documents.

Ce sera la fonction que nous utiliserons dans notre fichier racine, où nous avons créé le serveur Express pour nous assurer que les documents sont également démarrés.

// In src/index.js
const express = require("express");
const bodyParser = require("body-parser");
const v1WorkoutRouter = require("./v1/routes/workoutRoutes");
// *** ADD ***
const { swaggerDocs: V1SwaggerDocs } = require("./v1/swagger");

const app = express();
const PORT = process.env.PORT || 3000;

app.use(bodyParser.json());
app.use("/api/v1/workouts", v1WorkoutRouter);

app.listen(PORT, () => {
  console.log(`API is listening on port ${PORT}`);
  /// *** ADD ***
  V1SwaggerDocs(app, PORT);
});

Vous devriez maintenant voir à l'intérieur de votre terminal où tourne votre serveur de développement :

Capture d'écran-2022-04-28-at-20.23.51-1

Et lorsque vous visitez localhost:3000/api/v1/docs, vous devriez déjà voir notre page de documentation :

Capture d'écran-2022-04-28-at-20.25.00-1

Je suis étonné à chaque fois à quel point cela fonctionne bien. Maintenant, la structure de base est configurée et nous pouvons commencer à implémenter les documents pour nos terminaux. Allons-y!

Lorsque vous jetez un œil à options.apis dans notre fichier swagger.js, vous verrez que nous avons inclus le chemin vers nos itinéraires d'entraînement et vers le fichier d'entraînement dans notre dossier de base de données. C'est la chose la plus importante dans la configuration qui fera que toute la magie se produira.

Avoir ces fichiers définis dans nos options swagger nous permettra d'utiliser des commentaires faisant référence à OpenAPI et ayant une syntaxe comme dans les fichiers yaml, qui sont nécessaires pour configurer nos documents.

Nous sommes maintenant prêts à créer des documents pour notre premier point de terminaison ! Allons droit au but.

// In src/v1/routes/workoutRoutes.js
...

/**
 * @openapi
 * /api/v1/workouts:
 *   get:
 *     tags:
 *       - Workouts
 *     responses:
 *       200:
 *         description: OK
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 status:
 *                   type: string
 *                   example: OK
 *                 data:
 *                   type: array 
 *                   items: 
 *                     type: object
 */
router.get("/", cache("2 minutes"), workoutController.getAllWorkouts);

...

C'est fondamentalement toute la magie pour ajouter un point final à nos docs swagger. Vous pouvez rechercher toutes les spécifications pour décrire un point de terminaison dans leurs excellents documents .

Lorsque vous rechargez votre page de documents, vous devriez voir ce qui suit :

Capture d'écran-2022-04-29-at-07.21.51-1

Cela devrait vous sembler très familier si vous avez déjà travaillé avec des API disposant d'une documentation OpenAPI. C'est la vue où tous nos points de terminaison seront répertoriés et vous pouvez étendre chacun d'eux pour obtenir plus d'informations à son sujet.

Capture d'écran-2022-04-29-at-07.41.46-1

Lorsque vous examinez attentivement notre réponse, vous verrez que nous n'avons pas défini la valeur de retour correcte car nous disons simplement que notre propriété "data" sera un tableau d'objets vides.

C'est là que les schémas entrent en jeu.

// In src/databse/Workout.js
...

/**
 * @openapi
 * components:
 *   schemas:
 *     Workout:
 *       type: object
 *       properties:
 *         id: 
 *           type: string
 *           example: 61dbae02-c147-4e28-863c-db7bd402b2d6
 *         name: 
 *           type: string
 *           example: Tommy V  
 *         mode:
 *           type: string
 *           example: For Time
 *         equipment:
 *           type: array
 *           items:
 *             type: string
 *           example: ["barbell", "rope"]
 *         exercises:
 *           type: array
 *           items:
 *             type: string
 *           example: ["21 thrusters", "12 rope climbs, 15 ft", "15 thrusters", "9 rope climbs, 15 ft", "9 thrusters", "6 rope climbs, 15 ft"]
 *         createdAt:
 *           type: string
 *           example: 4/20/2022, 2:21:56 PM
 *         updatedAt: 
 *           type: string
 *           example: 4/20/2022, 2:21:56 PM
 *         trainerTips:
 *           type: array
 *           items:
 *             type: string
 *           example: ["Split the 21 thrusters as needed", "Try to do the 9 and 6 thrusters unbroken", "RX Weights: 115lb/75lb"]
 */

...

Dans l'exemple ci-dessus, nous avons créé notre premier schéma. Généralement, cette définition se trouve dans votre schéma ou fichier de modèle où vous avez défini vos modèles de base de données.

Comme vous pouvez le voir, c'est aussi assez simple. Nous avons défini toutes les propriétés qui composent un entraînement, y compris le type et un exemple.

Vous pouvez visiter à nouveau notre page de documentation et nous recevrons une autre section contenant nos schémas.

Capture d'écran-2022-04-29-at-07.29.49-1

Ce schéma peut être référencé maintenant dans notre réponse de notre point de terminaison.

// In src/v1/routes/workoutRoutes.js
...

/**
 * @openapi
 * /api/v1/workouts:
 *   get:
 *     tags:
 *       - Workouts
 *     responses:
 *       200:
 *         description: OK
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 status:
 *                   type: string
 *                   example: OK
 *                 data:
 *                   type: array 
 *                   items: 
 *                     $ref: "#/components/schemas/Workout"
 */
router.get("/", cache("2 minutes"), workoutController.getAllWorkouts);

...

Regardez attentivement le bas de notre commentaire sous "articles". Nous utilisons "$ref" pour créer une référence et référençons le chemin vers notre schéma que nous avons défini dans notre fichier d'entraînement.

Nous pouvons désormais afficher un entraînement complet dans notre réponse.

capture d'écran-2022-04-29-at-07.44.12-1

Plutôt cool, non ? Vous pourriez penser que "taper ces commentaires à la main peut être une tâche fastidieuse".

Cela pourrait être vrai, mais pensez à cette façon. Ces commentaires qui se trouvent dans votre base de code sont également une excellente documentation pour vous-même en tant que développeur d'API. Vous n'avez pas besoin de consulter la documentation tout le temps lorsque vous souhaitez connaître la documentation d'un point de terminaison spécifique. Vous pouvez simplement le rechercher à un endroit dans votre code source.

La documentation des endpoints vous aide également à mieux les comprendre et vous « oblige » à penser à tout ce que vous auriez pu oublier de mettre en œuvre.

Comme vous pouvez le voir, j'ai effectivement oublié quelque chose. Les réponses d'erreur possibles et les paramètres de requête sont toujours manquants !

Corrigeons ça :

// In src/v1/routes/workoutRoutes.js
...

/**
 * @openapi
 * /api/v1/workouts:
 *   get:
 *     tags:
 *       - Workouts
 *     parameters:
 *       - in: query
 *         name: mode
 *         schema:
 *           type: string
 *         description: The mode of a workout
 *     responses:
 *       200:
 *         description: OK
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 status:
 *                   type: string
 *                   example: OK
 *                 data:
 *                   type: array 
 *                   items: 
 *                     $ref: "#/components/schemas/Workout"
 *       5XX:
 *         description: FAILED
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 status: 
 *                   type: string
 *                   example: FAILED
 *                 data:
 *                   type: object
 *                   properties:
 *                     error:
 *                       type: string 
 *                       example: "Some error message"
 */
router.get("/", cache("2 minutes"),  workoutController.getAllWorkouts);

...

Lorsque vous regardez en haut de notre commentaire sous "tags", vous pouvez voir que j'ai ajouté une autre clé appelée "paramètres", où j'ai défini notre paramètre de requête pour le filtrage.

Nos docs l'affichent désormais correctement :

Capture d'écran-2022-04-29-at-08.03.00-1

Et pour documenter un cas d'erreur possible, nous ne lançons qu'une erreur 5XX à ce stade. Donc, sous "réponses", vous pouvez voir que j'ai également défini une autre documentation pour cela.

Sur notre page de documentation, cela ressemble à ceci :

Capture d'écran-2022-04-29-at-08.04.44-2

Étonnante! Nous venons de créer la documentation complète pour un endpoint. Je vous recommande fortement d'implémenter vous-même le reste des points de terminaison pour vous salir les mains. Vous apprendrez beaucoup dans le processus !

Comme vous avez pu le constater, documenter votre API ne doit pas toujours être un casse-tête. Je pense que les outils que je vous ai présentés pour réduire votre effort global, et tout mettre en place est assez simple.

Nous pouvons donc nous concentrer sur l'essentiel, la documentation elle-même. À mon avis, la documentation de swagger/OpenAPI est très bonne et il existe de nombreux exemples intéressants sur Internet.

Ne pas avoir de documentation à cause de trop de travail "supplémentaire" ne devrait plus être une raison.

Conclusion

Puuh, c'était une balade assez amusante. J'ai vraiment aimé écrire cet article pour vous et j'ai aussi beaucoup appris.

Certaines pratiques exemplaires peuvent être importantes tandis que d'autres peuvent sembler ne pas s'appliquer à votre situation actuelle. C'est bien, car comme je l'ai dit plus tôt, il est de la responsabilité de chaque ingénieur de choisir les meilleures pratiques qui peuvent être appliquées à sa situation actuelle.

J'ai fait de mon mieux pour fusionner toutes ces meilleures pratiques que j'ai élaborées jusqu'à présent tout en créant notre propre API en cours de route. Cela m'a beaucoup amusé!

J'aimerais recevoir des commentaires de toute sorte. S'il y a quelque chose que vous voudriez me dire (en bien ou en mal), n'hésitez pas à me contacter 

Voici mon Instagram (vous pouvez également suivre mon parcours en tant que développeur de logiciels)

Link: https://www.freecodecamp.org/news/rest-api-design-best-practices-build-a-rest-api/

#api #rest #restapi #javascript #node #nodejs #express #expressjs 

NBB: Ad-hoc CLJS Scripting on Node.js

Nbb

Not babashka. Node.js babashka!?

Ad-hoc CLJS scripting on Node.js.

Status

Experimental. Please report issues here.

Goals and features

Nbb's main goal is to make it easy to get started with ad hoc CLJS scripting on Node.js.

Additional goals and features are:

  • Fast startup without relying on a custom version of Node.js.
  • Small artifact (current size is around 1.2MB).
  • First class macros.
  • Support building small TUI apps using Reagent.
  • Complement babashka with libraries from the Node.js ecosystem.

Requirements

Nbb requires Node.js v12 or newer.

How does this tool work?

CLJS code is evaluated through SCI, the same interpreter that powers babashka. Because SCI works with advanced compilation, the bundle size, especially when combined with other dependencies, is smaller than what you get with self-hosted CLJS. That makes startup faster. The trade-off is that execution is less performant and that only a subset of CLJS is available (e.g. no deftype, yet).

Usage

Install nbb from NPM:

$ npm install nbb -g

Omit -g for a local install.

Try out an expression:

$ nbb -e '(+ 1 2 3)'
6

And then install some other NPM libraries to use in the script. E.g.:

$ npm install csv-parse shelljs zx

Create a script which uses the NPM libraries:

(ns script
  (:require ["csv-parse/lib/sync$default" :as csv-parse]
            ["fs" :as fs]
            ["path" :as path]
            ["shelljs$default" :as sh]
            ["term-size$default" :as term-size]
            ["zx$default" :as zx]
            ["zx$fs" :as zxfs]
            [nbb.core :refer [*file*]]))

(prn (path/resolve "."))

(prn (term-size))

(println (count (str (fs/readFileSync *file*))))

(prn (sh/ls "."))

(prn (csv-parse "foo,bar"))

(prn (zxfs/existsSync *file*))

(zx/$ #js ["ls"])

Call the script:

$ nbb script.cljs
"/private/tmp/test-script"
#js {:columns 216, :rows 47}
510
#js ["node_modules" "package-lock.json" "package.json" "script.cljs"]
#js [#js ["foo" "bar"]]
true
$ ls
node_modules
package-lock.json
package.json
script.cljs

Macros

Nbb has first class support for macros: you can define them right inside your .cljs file, like you are used to from JVM Clojure. Consider the plet macro to make working with promises more palatable:

(defmacro plet
  [bindings & body]
  (let [binding-pairs (reverse (partition 2 bindings))
        body (cons 'do body)]
    (reduce (fn [body [sym expr]]
              (let [expr (list '.resolve 'js/Promise expr)]
                (list '.then expr (list 'clojure.core/fn (vector sym)
                                        body))))
            body
            binding-pairs)))

Using this macro we can look async code more like sync code. Consider this puppeteer example:

(-> (.launch puppeteer)
      (.then (fn [browser]
               (-> (.newPage browser)
                   (.then (fn [page]
                            (-> (.goto page "https://clojure.org")
                                (.then #(.screenshot page #js{:path "screenshot.png"}))
                                (.catch #(js/console.log %))
                                (.then #(.close browser)))))))))

Using plet this becomes:

(plet [browser (.launch puppeteer)
       page (.newPage browser)
       _ (.goto page "https://clojure.org")
       _ (-> (.screenshot page #js{:path "screenshot.png"})
             (.catch #(js/console.log %)))]
      (.close browser))

See the puppeteer example for the full code.

Since v0.0.36, nbb includes promesa which is a library to deal with promises. The above plet macro is similar to promesa.core/let.

Startup time

$ time nbb -e '(+ 1 2 3)'
6
nbb -e '(+ 1 2 3)'   0.17s  user 0.02s system 109% cpu 0.168 total

The baseline startup time for a script is about 170ms seconds on my laptop. When invoked via npx this adds another 300ms or so, so for faster startup, either use a globally installed nbb or use $(npm bin)/nbb script.cljs to bypass npx.

Dependencies

NPM dependencies

Nbb does not depend on any NPM dependencies. All NPM libraries loaded by a script are resolved relative to that script. When using the Reagent module, React is resolved in the same way as any other NPM library.

Classpath

To load .cljs files from local paths or dependencies, you can use the --classpath argument. The current dir is added to the classpath automatically. So if there is a file foo/bar.cljs relative to your current dir, then you can load it via (:require [foo.bar :as fb]). Note that nbb uses the same naming conventions for namespaces and directories as other Clojure tools: foo-bar in the namespace name becomes foo_bar in the directory name.

To load dependencies from the Clojure ecosystem, you can use the Clojure CLI or babashka to download them and produce a classpath:

$ classpath="$(clojure -A:nbb -Spath -Sdeps '{:aliases {:nbb {:replace-deps {com.github.seancorfield/honeysql {:git/tag "v2.0.0-rc5" :git/sha "01c3a55"}}}}}')"

and then feed it to the --classpath argument:

$ nbb --classpath "$classpath" -e "(require '[honey.sql :as sql]) (sql/format {:select :foo :from :bar :where [:= :baz 2]})"
["SELECT foo FROM bar WHERE baz = ?" 2]

Currently nbb only reads from directories, not jar files, so you are encouraged to use git libs. Support for .jar files will be added later.

Current file

The name of the file that is currently being executed is available via nbb.core/*file* or on the metadata of vars:

(ns foo
  (:require [nbb.core :refer [*file*]]))

(prn *file*) ;; "/private/tmp/foo.cljs"

(defn f [])
(prn (:file (meta #'f))) ;; "/private/tmp/foo.cljs"

Reagent

Nbb includes reagent.core which will be lazily loaded when required. You can use this together with ink to create a TUI application:

$ npm install ink

ink-demo.cljs:

(ns ink-demo
  (:require ["ink" :refer [render Text]]
            [reagent.core :as r]))

(defonce state (r/atom 0))

(doseq [n (range 1 11)]
  (js/setTimeout #(swap! state inc) (* n 500)))

(defn hello []
  [:> Text {:color "green"} "Hello, world! " @state])

(render (r/as-element [hello]))

Promesa

Working with callbacks and promises can become tedious. Since nbb v0.0.36 the promesa.core namespace is included with the let and do! macros. An example:

(ns prom
  (:require [promesa.core :as p]))

(defn sleep [ms]
  (js/Promise.
   (fn [resolve _]
     (js/setTimeout resolve ms))))

(defn do-stuff
  []
  (p/do!
   (println "Doing stuff which takes a while")
   (sleep 1000)
   1))

(p/let [a (do-stuff)
        b (inc a)
        c (do-stuff)
        d (+ b c)]
  (prn d))
$ nbb prom.cljs
Doing stuff which takes a while
Doing stuff which takes a while
3

Also see API docs.

Js-interop

Since nbb v0.0.75 applied-science/js-interop is available:

(ns example
  (:require [applied-science.js-interop :as j]))

(def o (j/lit {:a 1 :b 2 :c {:d 1}}))

(prn (j/select-keys o [:a :b])) ;; #js {:a 1, :b 2}
(prn (j/get-in o [:c :d])) ;; 1

Most of this library is supported in nbb, except the following:

  • destructuring using :syms
  • property access using .-x notation. In nbb, you must use keywords.

See the example of what is currently supported.

Examples

See the examples directory for small examples.

Also check out these projects built with nbb:

API

See API documentation.

Migrating to shadow-cljs

See this gist on how to convert an nbb script or project to shadow-cljs.

Build

Prequisites:

  • babashka >= 0.4.0
  • Clojure CLI >= 1.10.3.933
  • Node.js 16.5.0 (lower version may work, but this is the one I used to build)

To build:

  • Clone and cd into this repo
  • bb release

Run bb tasks for more project-related tasks.

Download Details:
Author: borkdude
Download Link: Download The Source Code
Official Website: https://github.com/borkdude/nbb 
License: EPL-1.0

#node #javascript

Wilford  Pagac

Wilford Pagac

1594289280

What is REST API? An Overview | Liquid Web

What is REST?

The REST acronym is defined as a “REpresentational State Transfer” and is designed to take advantage of existing HTTP protocols when used for Web APIs. It is very flexible in that it is not tied to resources or methods and has the ability to handle different calls and data formats. Because REST API is not constrained to an XML format like SOAP, it can return multiple other formats depending on what is needed. If a service adheres to this style, it is considered a “RESTful” application. REST allows components to access and manage functions within another application.

REST was initially defined in a dissertation by Roy Fielding’s twenty years ago. He proposed these standards as an alternative to SOAP (The Simple Object Access Protocol is a simple standard for accessing objects and exchanging structured messages within a distributed computing environment). REST (or RESTful) defines the general rules used to regulate the interactions between web apps utilizing the HTTP protocol for CRUD (create, retrieve, update, delete) operations.

What is an API?

An API (or Application Programming Interface) provides a method of interaction between two systems.

What is a RESTful API?

A RESTful API (or application program interface) uses HTTP requests to GET, PUT, POST, and DELETE data following the REST standards. This allows two pieces of software to communicate with each other. In essence, REST API is a set of remote calls using standard methods to return data in a specific format.

The systems that interact in this manner can be very different. Each app may use a unique programming language, operating system, database, etc. So, how do we create a system that can easily communicate and understand other apps?? This is where the Rest API is used as an interaction system.

When using a RESTful API, we should determine in advance what resources we want to expose to the outside world. Typically, the RESTful API service is implemented, keeping the following ideas in mind:

  • Format: There should be no restrictions on the data exchange format
  • Implementation: REST is based entirely on HTTP
  • Service Definition: Because REST is very flexible, API can be modified to ensure the application understands the request/response format.
  • The RESTful API focuses on resources and how efficiently you perform operations with it using HTTP.

The features of the REST API design style state:

  • Each entity must have a unique identifier.
  • Standard methods should be used to read and modify data.
  • It should provide support for different types of resources.
  • The interactions should be stateless.

For REST to fit this model, we must adhere to the following rules:

  • Client-Server Architecture: The interface is separate from the server-side data repository. This affords flexibility and the development of components independently of each other.
  • Detachment: The client connections are not stored on the server between requests.
  • Cacheability: It must be explicitly stated whether the client can store responses.
  • Multi-level: The API should work whether it interacts directly with a server or through an additional layer, like a load balancer.

#tutorials #api #application #application programming interface #crud #http #json #programming #protocols #representational state transfer #rest #rest api #rest api graphql #rest api json #rest api xml #restful #soap #xml #yaml

An API-First Approach For Designing Restful APIs | Hacker Noon

I’ve been working with Restful APIs for some time now and one thing that I love to do is to talk about APIs.

So, today I will show you how to build an API using the API-First approach and Design First with OpenAPI Specification.

First thing first, if you don’t know what’s an API-First approach means, it would be nice you stop reading this and check the blog post that I wrote to the Farfetchs blog where I explain everything that you need to know to start an API using API-First.

Preparing the ground

Before you get your hands dirty, let’s prepare the ground and understand the use case that will be developed.

Tools

If you desire to reproduce the examples that will be shown here, you will need some of those items below.

  • NodeJS
  • OpenAPI Specification
  • Text Editor (I’ll use VSCode)
  • Command Line

Use Case

To keep easy to understand, let’s use the Todo List App, it is a very common concept beyond the software development community.

#api #rest-api #openai #api-first-development #api-design #apis #restful-apis #restful-api

Lets Cms

Lets Cms

1652251629

Unilevel MLM Wordpress Rest API FrontEnd | UMW Rest API Woocommerce

Unilevel MLM Wordpress Rest API FrontEnd | UMW Rest API Woocommerce Price USA, Philippines : Our API’s handle the Unilevel MLM woo-commerce end user all functionalities like customer login/register. You can request any type of information which is listed below, our API will provide you managed results for your all frontend needs, which will be useful for your applications like Mobile App etc.
Business to Customer REST API for Unilevel MLM Woo-Commerce will empower your Woo-commerce site with the most powerful Unilevel MLM Woo-Commerce REST API, you will be able to get and send data to your marketplace from other mobile apps or websites using HTTP Rest API request.
Our plugin is used JWT authentication for the authorization process.

REST API Unilevel MLM Woo-commerce plugin contains following APIs.
User Login Rest API
User Register Rest API
User Join Rest API
Get User info Rest API
Get Affiliate URL Rest API 
Get Downlines list Rest API
Get Bank Details Rest API
Save Bank Details Rest API
Get Genealogy JSON Rest API
Get Total Earning Rest API
Get Current Balance Rest API
Get Payout Details Rest API
Get Payout List Rest API
Get Commissions List Rest API
Withdrawal Request Rest API
Get Withdrawal List Rest API

If you want to know more information and any queries regarding Unilevel MLM Rest API Woocommerce WordPress Plugin, you can contact our experts through 
Skype: jks0586, 
Mail: letscmsdev@gmail.com,
Website: www.letscms.com, www.mlmtrees.com,
Call/WhatsApp/WeChat: +91-9717478599.  

more information : https://www.mlmtrees.com/product/unilevel-mlm-woocommerce-rest-api-addon

Visit Documentation : https://letscms.com/documents/umw_apis/umw-apis-addon-documentation.html

#Unilevel_MLM_WooCommerce_Rest_API's_Addon #umw_mlm_rest_api #rest_api_woocommerce_unilevel #rest_api_in_woocommerce #rest_api_woocommerce #rest_api_woocommerce_documentation #rest_api_woocommerce_php #api_rest_de_woocommerce #woocommerce_rest_api_in_android #woocommerce_rest_api_in_wordpress #Rest_API_Woocommerce_unilevel_mlm #wp_rest_api_woocommerce