Article original : REST API Design Best Practices Handbook – How to Build a REST API with JavaScript, Node.js, and Express.js

Par Jean-Marc M6ckel

J'ai cr99 et consomm9 de nombreuses API au cours des derni8res ann9es. Pendant cette p9riode, j'ai rencontr9 de bonnes et de mauvaises pratiques et j'ai v9cu des situations d9sagr9ables lors de la consommation et de la cr9ation d'API. Mais il y a aussi eu de grands moments.

Il existe des articles utiles en ligne qui pr9sentent de nombreuses bonnes pratiques, mais beaucoup d'entre eux manquent, 0 mon avis, de pragmatisme. Connaetre la th9orie avec quelques exemples est bien, mais je me suis toujours demand9 comment l'impl9mentation se pr9senterait dans un exemple plus r9aliste.

Fournir des exemples simples aide 0 comprendre le concept lui-mame sans trop de complexit9, mais en pratique, les choses ne sont pas toujours si simples. Je suis sbr que vous savez de quoi je parle 0991

C'est pourquoi j'ai d9cid9 d'9crire ce tutoriel. J'ai fusionn9 toutes ces apprentissages (bons et mauvais) en un seul article digestible tout en fournissant un exemple pratique qui peut atre suivi. 0 la fin, nous construirons une API compl8te tout en impl9mentant une bonne pratique apr8s l'autre.

Quelques points 0 retenir avant de commencer :

Les bonnes pratiques sont, comme vous l'avez peut-atre devin9, pas des lois ou des r8gles sp9cifiques 0 suivre. Ce sont des conventions ou des conseils qui ont 9volu9 au fil du temps et se sont av9r9s efficaces. Certaines sont devenues des standards de nos jours. Mais cela ne signifie pas que vous devez les adapter 0 l'identique.

Elles devraient vous donner une direction pour am9liorer vos API en termes d'exp9rience utilisateur (pour le consommateur et le constructeur), de s9curit9 et de performance.

Gardez simplement 0 l'esprit que les projets sont diff9rents et n9cessitent des approches diff9rentes. Il peut y avoir des situations o9 vous ne pouvez pas ou ne devez pas suivre une certaine convention. Chaque ing9nieur doit donc d9cider cela pour lui-mame ou avec son 9quipe.

Maintenant que nous avons 9clairci ces points, sans plus attendre, mettons-nous au travail !

Table des mati8res

Notre projet d'exemple

Image _Photo par [Unsplash](https://unsplash.com/@alvarordesign?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Alvaro Reyes sur <a href="https://unsplash.com/s/photos/project?utm_source=unsplash&utm_medium=referral&utmcontent=creditCopyText)

Avant de commencer 0 impl9menter les meilleures pratiques dans notre projet d'exemple, je voudrais vous donner une br8ve introduction de ce que nous allons construire.

Nous allons construire une API REST pour une application d'entraenement CrossFit. Si vous n'ates pas familier avec le CrossFit, c'est une m9thode de fitness et un sport comp9titif qui combine des entraenements de haute intensit9 avec des 9l9ments de plusieurs sports (halt9rophilie olympique, gymnastique, et autres).

Dans notre application, nous aimerions cr9er, lire, mettre 0 jour et supprimer des WOD (Workouts of the Day). Cela aidera nos utilisateurs (qui seront des propri9taires de salles de sport) 0 9laborer des plans d'entraenement et 0 maintenir leurs propres entraenements dans une seule application. En plus de cela, ils peuvent 9galement ajouter des conseils d'entraenement importants pour chaque s9ance.

Notre travail consistera 0 concevoir et impl9menter une API pour cette application.

Pr9requis

Pour suivre ce tutoriel, vous devez avoir une certaine exp9rience en JavaScript, Node.js, Express.js et en architecture backend. Des termes comme REST et API ne devraient pas atre nouveaux pour vous et vous devriez avoir une compr9hension du mod8le Client-Serveur.

Bien sbr que vous n'ayez pas besoin d'atre un expert dans ces domaines, une certaine familiarit9 et id9alement une certaine exp9rience devraient suffire.

Mame si tous les pr9requis ne s'appliquent pas 0 vous, ce n'est bien sbr pas une raison de sauter ce tutoriel. Il y a encore beaucoup 0 apprendre ici pour vous. Mais avoir ces comp9tences rendra les choses plus faciles pour vous.

Mame si cette API est 9crite en JavaScript et Express, les bonnes pratiques ne sont pas limit9es 0 ces outils. Elles peuvent atre appliqu9es 0 d'autres langages de programmation ou frameworks.

Architecture

Comme discut9 pr9c9demment, nous utiliserons Express.js pour notre API. Je ne veux pas compliquer les choses avec une architecture complexe, donc je souhaite rester avec l'architecture en 3 couches :

Image

Dans le Contr4leur, nous g9rerons tout ce qui est li9 au HTTP. Cela signifie que nous traitons les requates et les r9ponses pour nos endpoints. Au-dessus de cette couche se trouve 9galement un petit Routeur d'Express qui transmet les requates au contr4leur correspondant.

Toute la logique m9tier sera dans la Couche de Service qui exporte certains services (m9thodes) utilis9s par le contr4leur.

La troisi8me couche est la Couche d'Acc8s aux Donn9es o9 nous travaillerons avec notre base de donn9es. Nous exporterons certaines m9thodes pour des op9rations de base de donn9es sp9cifiques comme la cr9ation d'un WOD qui peuvent atre utilis9es par notre Couche de Service.

Dans notre exemple, nous n'utilisons pas de base de donn9es r9elle comme MongoDB ou PostgreSQL car je souhaite me concentrer davantage sur les bonnes pratiques elles-mames. Par cons9quent, nous utilisons un fichier JSON local qui imite notre base de donn9es. Mais cette logique peut atre transf9r9e 0 d'autres bases de donn9es bien sbr.

Installation de base

Maintenant, nous devrions atre prats 0 cr9er une installation de base pour notre API. Nous ne compliquerons pas trop les choses et nous construirons une structure de projet simple mais organis9e.

Tout d'abord, cr9ons la structure globale des dossiers avec tous les fichiers et d9pendances n9cessaires. Apr8s cela, nous ferons un test rapide pour v9rifier si tout fonctionne correctement :

# Cr9er un dossier de projet et naviguer dedans
mkdir crossfit-wod-api && cd crossfit-wod-api
# Cr9er un dossier src et naviguer dedans
mkdir src && cd src
# Cr9er des sous-dossiers
mkdir controllers && mkdir services && mkdir database && mkdir routes
# Cr9er un fichier index (point d'entr9e de notre API)
touch index.js
# Nous sommes actuellement dans le dossier src, donc nous devons remonter d'un niveau
cd .. 

# Cr9er un fichier package.json 
npm init -y

Installer les d9pendances pour l'installation de base :

# D9pendances de d9veloppement 
npm i -D nodemon 

# D9pendances 
npm i express

Ouvrez le projet dans votre 9diteur de texte pr9f9r9 et configurez Express :

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

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

// Pour des fins de test 
app.get("/", (req, res) => { 
    res.send("<h2>Cela fonctionne !</h2>"); 
}); 

app.listen(PORT, () => { 
    console.log(`L'API 9coute sur le port ${PORT}`); 
});

Int9grez un nouveau script appel9 "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 garantit que le serveur de d9veloppement red9marre automatiquement lorsque nous apportons des modifications (gr2ce 0 nodemon).

Lancez le serveur de d9veloppement :

npm run dev

Regardez votre terminal, et vous devriez voir un message indiquant que "L'API 9coute sur le port 3000".

Visitez localhost:3000 dans votre navigateur. Si tout est configur9 correctement, vous devriez voir ce qui suit :

Image

G9nial ! Nous sommes maintenant prats 0 impl9menter les meilleures pratiques.

Meilleures pratiques pour les API REST

Image _Photo par [Unsplash](https://unsplash.com/@conniwenningsimages?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Constantin Wenning sur <a href="https://unsplash.com/s/photos/handshake?utm_source=unsplash&utm_medium=referral&utmcontent=creditCopyText)

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

Commen7ons simplement avec nos endpoints CRUD fondamentaux. Apr8s cela, nous 9tendrons l'API avec chaque bonne pratique.

Versionnement

Attendez une seconde. Avant d'9crire du code sp9cifique 0 l'API, nous devons atre conscients du versionnement. Comme dans d'autres applications, il y aura des am9liorations, de nouvelles fonctionnalit9s, et des choses comme 7a. Il est donc important de versionner notre API 9galement.

Le grand avantage est que nous pouvons travailler sur de nouvelles fonctionnalit9s ou am9liorations sur une nouvelle version tandis que les clients utilisent toujours la version actuelle et ne sont pas affect9s par les changements cassants.

Nous ne for7ons pas non plus les clients 0 utiliser la nouvelle version tout de suite. Ils peuvent utiliser la version actuelle et migrer par eux-mames lorsque la nouvelle version est stable.

Les versions actuelle et nouvelle fonctionnent essentiellement en parall8le et ne s'affectent pas mutuellement.

Mais comment pouvons-nous diff9rencier les versions ? Une bonne pratique consiste 0 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 ext9rieur et ce qui peut atre consomm9 par d'autres d9veloppeurs. Mais nous devons 9galement structurer notre projet afin de diff9rencier chaque version.

Il existe de nombreuses approches diff9rentes pour g9rer le versionnement dans une API Express. Dans notre cas, j'aimerais cr9er un sous-dossier pour chaque version dans notre r9pertoire src appel9 v1.

mkdir src/v1

Maintenant, d9placons notre dossier routes dans ce nouveau r9pertoire v1.

# Obtenez le chemin de votre r9pertoire actuel (copiez-le) 
pwd 

# D9placez "routes" dans "v1" (ins9rez le chemin ci-dessus dans {pwd}) 
mv {pwd}/src/routes {pwd}/src/v1

Le nouveau r9pertoire /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.

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

0 l'int9rieur, nous d9marrons un simple routeur.

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

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

module.exports = router;

Maintenant, nous devons connecter notre routeur pour v1 dans notre point d'entr9e racine dans src/index.js.

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

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

// *** SUPPRIMER ***
app.get("/", (req, res) => {
  res.send("<h2>Cela fonctionne !</h2>");
});

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

app.listen(PORT, () => {
  console.log(`L'API 9coute sur le port ${PORT}`);
});

Cela s'est bien pass9, n'est-ce pas ? Maintenant, nous capturons toutes les requates qui vont vers /api/v1/workouts avec notre v1WorkoutRouter.

0 l'int9rieur de notre routeur, nous appellerons une m9thode diff9rente g9r9e par notre contr4leur pour chaque endpoint diff9rent.

Cr9ons une m9thode pour chaque endpoint. Envoyer un message en retour devrait suffire pour l'instant.

// Dans src/controllers/workoutController.js
const getAllWorkouts = (req, res) => {
  res.send("Obtenir tous les entraenements");
};

const getOneWorkout = (req, res) => {
  res.send("Obtenir un entraenement existant");
};

const createNewWorkout = (req, res) => {
  res.send("Cr9er un nouvel entraenement");
};

const updateOneWorkout = (req, res) => {
  res.send("Mettre 0 jour un entraenement existant");
};

const deleteOneWorkout = (req, res) => {
  res.send("Supprimer un entraenement existant");
};

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

Maintenant, il est temps de refactoriser un peu notre routeur d'entraenement et d'utiliser les m9thodes du contr4leur.

// Dans 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;

Maintenant, nous pouvons tester notre endpoint GET /api/v1/workouts/:workoutId en tapant localhost:3000/api/v1/workouts/2342 dans le navigateur. Vous devriez voir quelque chose comme ceci :

Image

Nous avons r9ussi ! La premi8re couche de notre architecture est termin9e. Cr9ons notre couche de service en impl9mentant la prochaine bonne pratique.

Accepter et r9pondre avec des donn9es au format JSON

Lorsque vous interagissez avec une API, vous envoyez toujours des donn9es sp9cifiques avec votre requate ou vous recevez des donn9es avec la r9ponse. Il existe de nombreux formats de donn9es diff9rents, mais JSON (JavaScript Object Notation) est un format standardis9.

Bien qu'il y ait le terme JavaScript dans JSON, il n'est pas sp9cifiquement li9 0 celui-ci. Vous pouvez 9galement 9crire votre API avec Java ou Python qui peuvent g9rer JSON 9galement.

En raison de sa standardisation, les API devraient accepter et r9pondre avec des donn9es au format JSON.

Examinons notre impl9mentation actuelle et voyons comment nous pouvons int9grer cette bonne pratique.

Tout d'abord, nous cr9ons notre couche de service.

// Dans 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,
};

Il est 9galement bon de nommer les m9thodes de service de la mame mani8re que les m9thodes du contr4leur afin d'avoir une connexion entre celles-ci. Commen7ons par ne rien retourner.

Dans notre contr4leur d'entraenement, nous pouvons utiliser ces m9thodes.

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

const getAllWorkouts = (req, res) => {
  // *** AJOUTER ***
  const allWorkouts = workoutService.getAllWorkouts();
  res.send("Obtenir tous les entraenements");
};

const getOneWorkout = (req, res) => {
  // *** AJOUTER ***
  const workout = workoutService.getOneWorkout();
  res.send("Obtenir un entraenement existant");
};

const createNewWorkout = (req, res) => {
  // *** AJOUTER ***
  const createdWorkout = workoutService.createNewWorkout();
  res.send("Cr9er un nouvel entraenement");
};

const updateOneWorkout = (req, res) => {
  // *** AJOUTER ***
  const updatedWorkout = workoutService.updateOneWorkout();
  res.send("Mettre 0 jour un entraenement existant");
};

const deleteOneWorkout = (req, res) => {
  // *** AJOUTER ***
  workoutService.deleteOneWorkout();
  res.send("Supprimer un entraenement existant");
};

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

Pour l'instant, rien ne devrait avoir chang9 dans nos r9ponses. Mais sous le capot, notre couche de contr4leur communique maintenant avec notre couche de service.

Dans nos m9thodes de service, nous g9rerons notre logique m9tier comme la transformation des structures de donn9es et la communication avec notre couche de base de donn9es.

Pour cela, nous avons besoin d'une base de donn9es et d'une collection de m9thodes qui g8rent effectivement l'interaction avec la base de donn9es. Notre base de donn9es sera un simple fichier JSON pr9-rempli avec quelques entraenements d9j0.

# Cr9er un nouveau fichier appel9 db.json dans src/database 
touch src/database/db.json 

# Cr9er un fichier Workout qui stocke toutes les m9thodes sp9cifiques aux entraenements dans /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 entraenements sont ins9r9s. Un entraenement se compose d'un id, d'un nom, d'un mode, d'9quipements, d'exercices, de createdAt, de updatedAt et de trainerTips.

Commen7ons par le plus simple et retournons tous les entraenements stock9s et commen7ons par impl9menter la m9thode correspondante dans notre couche d'acc8s aux donn9es (src/database/Workout.js).

Encore une fois, j'ai choisi de nommer la m9thode ici de la mame mani8re que celle du service et du contr4leur. Mais cela est totalement facultatif.

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

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

module.exports = { getAllWorkouts };

Sautez directement dans notre service d'entraenement et impl9mentez la logique pour getAllWorkouts.

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

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

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

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

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

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

Retourner tous les entraenements est assez simple et nous n'avons pas besoin de faire de transformations car c'est d9j0 un fichier JSON. Nous n'avons pas non plus besoin de prendre d'arguments pour l'instant. Donc cette impl9mentation est assez simple. Mais nous reviendrons sur cela plus tard.

Dans notre contr4leur d'entraenement, nous recevons la valeur de retour de workoutService.getAllWorkouts() et l'envoyons simplement comme r9ponse au client. Nous avons fait passer la r9ponse de la base de donn9es par notre service vers le contr4leur.

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

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

const getOneWorkout = (req, res) => {
  const workout = workoutService.getOneWorkout();
  res.send("Obtenir un entraenement existant");
};

const createNewWorkout = (req, res) => {
  const createdWorkout = workoutService.createNewWorkout();
  res.send("Cr9er un nouvel entraenement");
};

const updateOneWorkout = (req, res) => {
  const updatedWorkout = workoutService.updateOneWorkout();
  res.send("Mettre 0 jour un entraenement existant");
};

const deleteOneWorkout = (req, res) => {
  workoutService.deleteOneWorkout();
  res.send("Supprimer un entraenement existant");
};

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

Allez sur localhost:3000/api/v1/workouts dans votre navigateur et vous devriez voir la r9ponse JSON.

Image

Cela s'est bien pass9 ! Nous envoyons des donn9es au format JSON. Mais qu'en est-il de leur acceptation ? Pensons 0 un endpoint o9 nous devons recevoir des donn9es JSON du client. L'endpoint pour cr9er ou mettre 0 jour un entraenement a besoin de donn9es du client.

Dans notre contr4leur d'entraenement, nous extrayons le corps de la requate pour cr9er un nouvel entraenement et nous le transmettons au service d'entraenement. Dans le service d'entraenement, nous l'ins9rerons dans notre DB.json et enverrons le nouvel entraenement cr99 au client.

Pour pouvoir analyser le JSON envoy9 dans le corps de la requate, nous devons d'abord installer body-parser et le configurer.

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

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

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

app.listen(PORT, () => {
  console.log(`L'API 9coute sur le port ${PORT}`);
});

Maintenant, nous sommes en mesure de recevoir les donn9es JSON dans nos contr4leurs sous req.body.

Pour le tester correctement, ouvrez simplement votre client HTTP pr9f9r9 (j'utilise Postman), cr9ez une requate POST vers localhost:3000/api/v1/workouts et un corps de requate 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-atre remarqu9, certaines propri9t9s sont manquantes comme "id", "createdAt" et "updatedAt". C'est le travail de notre API d'ajouter ces propri9t9s avant de les ins9rer. Nous nous en occuperons dans notre service d'entraenement plus tard.

Dans la m9thode createNewWorkout de notre contr4leur d'entraenement, nous pouvons extraire le corps de l'objet de requate, faire une validation et le transmettre comme argument 0 notre service d'entraenement.

// Dans src/controllers/workoutController.js
...

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

...

Pour am9liorer la validation des requates, vous devriez normalement utiliser un package tiers comme express-validator.

Allons dans notre service d'entraenement et recevons les donn9es dans notre m9thode createNewWorkout.

Apr8s cela, nous ajoutons les propri9t9s manquantes 0 l'objet et les transmettons 0 une nouvelle m9thode dans notre couche d'acc8s aux donn9es pour les stocker dans notre DB.

Tout d'abord, nous cr9ons une simple fonction utilitaire pour 9craser notre fichier JSON afin de persister les donn9es.

# Cr9er un fichier utils dans notre r9pertoire de base de donn9es 
touch src/database/utils.js
// Dans 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.

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


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

// *** AJOUTER ***
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,
  // *** AJOUTER ***
  createNewWorkout,
};

Cela s'est bien pass9 ! L'9tape suivante est d'utiliser les m9thodes de la base de donn9es dans notre service d'entraenement.

# Installer le package uuid 
npm i uuid
// Dans src/services/workoutService.js
// *** AJOUTER ***
const { v4: uuid } = require("uuid");
// *** AJOUTER ***
const Workout = require("../database/Workout");

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

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

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

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

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

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

Waouh ! C'9tait amusant, n'est-ce pas ? Maintenant, vous pouvez aller dans votre client HTTP, envoyer la requate POST 0 nouveau, et vous devriez recevoir le nouvel entraenement cr99 en JSON.

Si vous essayez d'ajouter le mame entraenement une deuxi8me fois, vous recevez toujours un code de statut 201, mais sans le nouvel entraenement ins9r9.

Cela signifie que notre m9thode de base de donn9es annule l'insertion pour l'instant et ne retourne simplement rien. C'est parce que notre instruction if pour v9rifier s'il y a d9j0 un entraenement ins9r9 avec le mame nom entre en jeu. C'est bien pour l'instant, nous g9rerons ce cas dans la prochaine bonne pratique !

Maintenant, envoyez une requate GET 0 localhost:3000/api/v1/workouts pour lire tous les entraenements. Je choisis le navigateur pour cela. Vous devriez voir que notre entraenement a 9t9 ins9r9 et persist9 avec succ8s :

Image

Vous pouvez impl9menter les autres m9thodes par vous-mame ou simplement copier mes impl9mentations.

Tout d'abord, le contr4leur d'entraenement (vous pouvez simplement copier tout le contenu) :

// Dans 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'entraenement (vous pouvez simplement copier tout le contenu) :

// Dans 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 m9thodes de base de donn9es dans la couche d'acc8s aux donn9es (vous pouvez simplement copier tout le contenu) :

// Dans 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,
};

G9nial ! Passons 0 la prochaine bonne pratique et voyons comment nous pouvons g9rer les erreurs correctement.

R9pondre avec des codes d'erreur HTTP standard

Nous avons d9j0 parcouru un long chemin, mais nous n'avons pas encore fini. Notre API a maintenant la capacit9 de g9rer les op9rations CRUD de base avec le stockage des donn9es. C'est g9nial, mais pas vraiment id9al.

Pourquoi ? Laissez-moi expliquer.

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

Vous connaissez probablement ce sentiment 9trange lorsque les choses fonctionnent correctement d8s le d9part sans aucune erreur. C'est g9nial et agr9able, mais en tant que d9veloppeurs, nous sommes plus habitu9s aux choses qui ne fonctionnent pas correctement. 0991

Il en va de mame pour notre API. Nous devons g9rer certains cas qui peuvent mal se passer ou g9n9rer une erreur. Cela renforcera 9galement notre API.

Lorsque quelque chose ne va pas (soit de la requate, soit 0 l'int9rieur de notre API), nous envoyons des codes d'erreur HTTP. J'ai vu et utilis9 des API qui renvoyaient tout le temps un code d'erreur 400 lorsqu'une requate 9tait bogu9e, sans aucun message sp9cifique sur POURQUOI cette erreur s'est produite ou quelle 9tait l'erreur. Le d9bogage est alors devenu un cauchemar.

C'est la raison pour laquelle il est toujours bon de renvoyer des codes d'erreur HTTP appropri9s pour diff9rents cas. Cela aide le consommateur ou l'ing9nieur qui a construit l'API 0 identifier le probl8me plus facilement.

Pour am9liorer l'exp9rience, nous pouvons 9galement envoyer un message d'erreur rapide avec la r9ponse d'erreur. Mais comme je l'ai 9crit dans l'introduction, cela n'est pas toujours tr8s judicieux et doit atre consid9r9 par l'ing9nieur lui-mame.

Par exemple, renvoyer quelque chose comme "Le nom d'utilisateur est d9j0 inscrit" doit atre bien r9fl9chi car vous fournissez des informations sur vos utilisateurs que vous devriez vraiment cacher.

Dans notre API Crossfit, nous allons examiner l'endpoint de cr9ation et voir quelles erreurs peuvent survenir et comment nous pouvons les g9rer. 0 la fin de ce conseil, vous trouverez 9galement l'impl9mentation compl8te des autres endpoints.

Commen7ons par examiner notre m9thode createNewWorkout dans notre contr4leur d'entraenement :

// Dans 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 d9j0 intercept9 le cas o9 le corps de la requate n'est pas correctement construit et manque de cl9s que nous attendons.

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

// Dans 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 entraenement mais oublions de fournir la propri9t9 "mode" dans le corps de notre requate, nous devrions voir le message d'erreur accompagn9 du code d'erreur HTTP 400.

Image

Un d9veloppeur qui consomme l'API est maintenant mieux inform9 sur ce qu'il doit rechercher. Il sait imm9diatement qu'il doit aller dans le corps de la requate et v9rifier s'il a oubli9 de fournir l'une des propri9t9s requises.

Laisser ce message d'erreur plus g9n9rique pour toutes les propri9t9s sera acceptable pour l'instant. Typiquement, vous utiliseriez un validateur de sch9ma pour g9rer cela.

Allons un niveau plus profond dans notre service d'entraenement et voyons quelles erreurs potentielles peuvent survenir.

// Dans 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 peut mal se passer est l'insertion dans la base de donn9es Workout.createNewWorkout(). J'aime envelopper cela dans un bloc try/catch pour attraper l'erreur lorsqu'elle se produit.

// Dans 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;
  }
};

...

Toute erreur qui est lev9e dans notre m9thode Workout.createNewWorkout() sera captur9e dans notre bloc catch. Nous la relan7ons simplement, afin de pouvoir ajuster nos r9ponses plus tard dans notre contr4leur.

D9finissons nos erreurs dans Workout.js :

// Dans 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-cl9 throw ici pour envoyer une structure de donn9es diff9rente d'une chaene, ce qui est requis dans throw new Error().

Un petit inconv9nient de simplement lancer est que nous n'obtenons pas de trace de pile. Mais normalement, ce lancement d'erreur serait g9r9 par une biblioth8que tierce de notre choix (par exemple Mongoose si vous utilisez une base de donn9es MongoDB). Mais pour les besoins de ce tutoriel, cela devrait suffire.

Maintenant, nous sommes en mesure de lancer et de capturer des erreurs dans le service et la couche d'acc8s aux donn9es. Nous pouvons passer dans notre contr4leur d'entraenement maintenant, capturer les erreurs l0-bas 9galement, et r9pondre en cons9quence.

// Dans 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,
  };
  // *** AJOUTER ***
  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 un entraenement avec le mame nom deux fois ou en ne fournissant pas une propri9t9 requise dans le corps de votre requate. 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 m9thodes impl9ment9es dans les fichiers suivants ou vous pouvez essayer par vous-mame :

// Dans 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,
};
// Dans 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,
};
// Dans 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,
};

9viter les verbes dans les noms des endpoints

Il n'est pas tr8s logique d'utiliser des verbes dans vos endpoints et c'est en fait assez inutile. G9n9ralement, chaque URL devrait pointer vers une ressource (rappelons l'exemple de la boete pr9c9demment mentionn9e). Rien de plus, rien de moins.

Utiliser un verbe dans une URL montre un certain comportement qu'une ressource elle-mame ne peut pas avoir.

Nous avons d9j0 impl9ment9 les endpoints correctement sans utiliser de verbes dans l'URL, mais examinons comment nos URL auraient l'air si nous avions utilis9 des verbes.

// Impl9mentations actuelles (sans verbes)
GET "/api/v1/workouts" 
GET "/api/v1/workouts/:workoutId" 
POST "/api/v1/workouts" 
PATCH "/api/v1/workouts/:workoutId" 
DELETE "/api/v1/workouts/:workoutId"  

// Impl9mentation utilisant des verbes 
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 diff9rence ? Avoir une URL compl8tement diff9rente pour chaque comportement peut devenir confus et inutilement complexe tr8s rapidement.

Imaginez que nous avons 300 endpoints diff9rents. Utiliser une URL s9par9e pour chacun pourrait atre un cauchemar (et de documentation).

Une autre raison que je voudrais souligner pour ne pas utiliser de verbes dans votre URL est que le verbe HTTP lui-mame indique d9j0 l'action.

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

Lorsque vous regardez notre impl9mentation actuelle, cela devient beaucoup plus propre car nous n'utilisons que deux URL diff9rentes et le comportement r9el est g9r9 via le verbe HTTP et la charge utile de la requate correspondante.

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

Grouper les ressources associ9es ensemble (imbrication logique)

Lorsque vous concevez votre API, il peut y avoir des cas o9 vous avez des ressources associ9es 0 d'autres. Il est bon de les regrouper dans un seul endpoint et de les imbriquer correctement.

Supposons que dans notre API, nous avons 9galement une liste de membres inscrits dans notre CrossFit box ("box" est le nom pour une salle de sport CrossFit). Afin de motiver nos membres, nous suivons les records globaux de la box pour chaque entraenement.

Par exemple, il y a un entraenement o9 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 des temps pour chaque membre qui a compl9t9 cet entraenement.

Maintenant, le frontend a besoin d'un endpoint qui r9ponde avec tous les records pour un entraenement sp9cifique afin de l'afficher dans l'interface utilisateur.

Les entraenements, les membres et les records sont stock9s dans diff9rents endroits de la base de donn9es. Donc, ce dont nous avons besoin ici, c'est une box (records) dans une autre box (entraenements), n'est-ce pas ?

L'URI pour cet endpoint sera /api/v1/workouts/:workoutId/records. C'est une bonne pratique de permettre l'imbrication logique des URL. L'URL elle-mame ne doit pas n9cessairement refl9ter la structure de la base de donn9es.

Commen7ons 0 impl9menter cet endpoint.

Tout d'abord, ajoutez un nouveau tableau dans votre db.json appel9 "members". Placez-le sous "workouts".

{
  "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 que vous ne commen7iez 0 demander, oui, les mots de passe sont hach9s. 0999

Apr8s cela, ajoutez quelques "records" sous "members".

{
  "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 mames entraenements que moi avec les mames identifiants, copiez 9galement les entraenements :

{
  "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": [ ...
  ]
}

D'accord, prenons quelques minutes pour r9fl9chir 0 notre impl9mentation.

Nous avons une ressource appel9e "workouts" d'un c4t9 et une autre appel9e "records" de l'autre c4t9.

Pour avancer dans notre architecture, il serait conseill9 de cr9er un autre contr4leur, un autre service et une autre collection de m9thodes de base de donn9es responsables des records.

Il est probable que nous devions impl9menter des endpoints CRUD pour les records 9galement, car les records doivent atre ajout9s, mis 0 jour ou supprim9s 0 l'avenir. Mais ce ne sera pas la t2che principale pour l'instant.

Nous aurons 9galement besoin d'un routeur de records pour capturer les requates sp9cifiques aux records, mais nous n'en avons pas besoin pour l'instant. Cela pourrait atre une excellente opportunit9 pour vous d'impl9menter les op9rations CRUD pour les records avec leurs propres routes et de vous entraener un peu.

# Cr9er un contr4leur de records 
touch src/controllers/recordController.js 

# Cr9er un service de records 
touch src/services/recordService.js 

# Cr9er des m9thodes de base de donn9es pour les records 
touch src/database/Record.js

C'9tait facile. Continuons et commen7ons par l'impl9mentation de nos m9thodes de base de donn9es.

// Dans 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, n'est-ce pas ? Nous filtrons tous les records qui sont li9s 0 l'identifiant de l'entraenement dans le param8tre de la requate.

Le suivant est notre service de records :

// Dans 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.

Maintenant, nous sommes en mesure de cr9er une nouvelle route dans notre routeur d'entraenement et de diriger la requate vers notre service de records.

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

const router = express.Router();

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

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

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

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

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

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

module.exports = router;

G9nial ! Testons les choses dans notre navigateur.

Tout d'abord, nous r9cup9rons tous les entraenements pour obtenir un identifiant d'entraenement.

Image

Voyons si nous pouvons r9cup9rer tous les records pour cela :

Image

Comme vous pouvez le voir, l'imbrication logique a du sens lorsque vous avez des ressources qui peuvent atre li9es ensemble. Th9oriquement, vous pouvez l'imbriquer aussi profond9ment que vous le souhaitez, mais la r8gle empirique ici est d'aller jusqu'0 trois niveaux de profondeur maximum.

Si vous souhaitez imbriquer plus profond9ment que cela, vous pourriez faire un petit ajustement dans vos enregistrements de base de donn9es. Je vais vous montrer un petit exemple.

Imaginez que le frontend a 9galement besoin d'un endpoint pour obtenir des informations sur quel membre d9tient exactement le record actuel et souhaite recevoir des m9tadonn9es 0 leur sujet.

Bien sbr, nous pourrions impl9menter l'URI suivante :

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

L'endpoint devient maintenant moins g9rable plus nous ajoutons d'imbrications. Par cons9quent, il est bon de stocker l'URI pour recevoir des informations sur un membre directement dans le record.

Consid9rez ce qui suit dans la base de donn9es :

{
  "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 ajout9 les deux propri9t9s "memberId" et "member" 0 nos records dans la base de donn9es. Cela pr9sente l'9norme avantage que nous n'avons pas besoin d'imbriquer plus profond9ment notre endpoint existant.

Le frontend doit simplement appeler GET /api/v1/workouts/:workoutId/records et re7oit automatiquement tous les records qui sont connect9s avec cet entraenement.

En plus de cela, il obtient l'identifiant du membre et l'endpoint pour r9cup9rer des informations sur ce membre. Ainsi, nous avons 9vit9 l'imbrication plus profonde de notre endpoint.

Bien sbr, cela ne fonctionne que si nous pouvons g9rer les requates vers "/members/:memberId" 0991 Cela semble atre une excellente opportunit9 de formation pour vous d'impl9menter cette situation !

Int9grer le filtrage, le tri et la pagination

Actuellement, nous sommes en mesure d'effectuer plusieurs op9rations avec notre API. C'est un grand progr8s, mais il y a plus.

Au cours des derni8res sections, nous nous sommes concentr9s sur l'am9lioration de l'exp9rience d9veloppeur et sur la mani8re dont notre API peut atre interagie. Mais la performance globale de notre API est un autre facteur cl9 que nous devons travailler.

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

Imaginez que nous avons 2 000 entraenements, 450 records et 500 membres stock9s dans notre DB. Lorsque nous appelons notre endpoint pour obtenir tous les entraenements, nous ne voulons pas envoyer les 2 000 entraenements en une seule fois. Cela sera bien sbr une r9ponse tr8s lente, ou cela fera planter nos syst8mes (peut-atre avec 200 000 0991).

C'est la raison pour laquelle le filtrage et la pagination sont importants. Le filtrage, comme le nom l'indique d9j0, est utile car il nous permet d'obtenir des donn9es sp9cifiques 0 partir de notre collection compl8te. Par exemple, tous les entraenements qui ont le mode "For Time".

La pagination est un autre m9canisme pour diviser notre collection compl8te d'entraenements en plusieurs "pages" o9 chaque page ne se compose que de vingt entraenements, par exemple. Cette technique nous aide 0 nous assurer que nous n'envoyons pas plus de vingt entraenements en mame temps avec notre r9ponse au client.

Le tri peut atre une t2che complexe. Il est donc plus efficace de le faire dans notre API et d'envoyer les donn9es tri9es au client.

Commen7ons par int9grer un m9canisme de filtrage dans notre API. Nous allons am9liorer notre endpoint qui envoie tous les entraenements en acceptant des param8tres de filtre. Normalement, dans une requate GET, nous ajoutons les crit8res de filtre en tant que param8tre de requate.

Notre nouvelle URI ressemblera 0 ceci, lorsque nous souhaitons obtenir uniquement les entraenements qui sont en mode "AMRAP" (As Many Rounds As Possible) : /api/v1/workouts?mode=amrap.

Pour rendre cela plus amusant, nous devons ajouter quelques entraenements suppl9mentaires. Collez ces entraenements dans votre collection "workouts" dans 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"
}

Apr8s cela, nous devons accepter et g9rer les param8tres de requate. Notre contr4leur d'entraenement sera le bon endroit pour commencer :

// Dans src/controllers/workoutController.js
...

const getAllWorkouts = (req, res) => {
  // *** AJOUTER ***
  const { mode } = req.query;
  try {
    // *** AJOUTER ***
    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 d9finissons un param8tre de workoutService.getAllWorkouts. Ce sera un objet qui se compose de nos param8tres de filtre.

J'utilise la syntaxe abr9g9e ici pour cr9er une nouvelle cl9 appel9e "mode" dans l'objet avec la valeur de ce qui se trouve dans "req.query.mode". Cela peut atre soit une valeur v9ridique, soit ind9finie s'il n'y a pas de param8tre de requate appel9 "mode". Nous pouvons 9tendre cet objet avec plus de param8tres de filtre que nous souhaitons accepter.

Dans notre service d'entraenement, transmettez-le 0 votre m9thode de base de donn9es :

// Dans src/services/workoutService.js
...

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

...

Maintenant, nous pouvons l'utiliser dans notre m9thode de base de donn9es et appliquer le filtrage :

// Dans 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)
      );
    }
    // D'autres instructions if iront ici pour diff9rents param8tres
    return workouts;
  } catch (error) {
    throw { status: 500, message: error };
  }
};

...

Assez simple, n'est-ce pas ? Tout ce que nous faisons ici est de v9rifier si nous avons effectivement une valeur v9ridique pour la cl9 "mode" dans notre "filterParams". Si c'est vrai, nous filtrons tous les entraenements qui ont le mame "mode". Si ce n'est pas vrai, alors il n'y a pas de param8tre de requate appel9 "mode" et nous retournons tous les entraenements car nous n'avons pas besoin de filtrer.

Nous avons d9fini "workouts" ici comme une variable "let" car lorsque nous ajoutons plus d'instructions if pour diff9rents filtres, nous pouvons 9craser "workouts" et enchaener les filtres.

Dans votre navigateur, vous pouvez visiter localhost:3000/api/v1/workouts?mode=amrap et vous recevrez tous les entraenements "AMRAP" stock9s :

Image

Si vous laissez le param8tre de requate vide, vous devriez obtenir tous les entraenements comme avant. Vous pouvez essayer davantage en ajoutant "for%20time" comme valeur pour le param8tre "mode" (rappel : "%20" signifie "espace") et vous devriez recevoir tous les entraenements qui ont le mode "For Time" s'il y en a stock9s.

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

Les param8tres pour le tri et la pagination suivent la mame philosophie. Examinons quelques fonctionnalit9s que nous pourrions 9ventuellement impl9menter :

  • Recevoir tous les entraenements qui n9cessitent une barre : /api/v1/workouts?equipment=barbell
  • Obtenir seulement 5 entraenements : /api/v1/workouts?length=5
  • Lorsque vous utilisez la pagination, recevoir la deuxi8me page : /api/v1/workouts?page=2
  • Trier les entraenements dans la r9ponse par ordre d9croissant selon leur date de cr9ation : /api/v1/workouts?sort=-createdAt
  • Vous pouvez 9galement combiner les param8tres, pour obtenir les 10 derniers entraenements mis 0 jour par exemple : /api/v1/workouts?sort=-updatedAt&length=10

Utiliser la mise en cache des donn9es pour am9liorer les performances

L'utilisation d'un cache de donn9es est 9galement une excellente pratique pour am9liorer l'exp9rience globale et les performances de notre API.

Il est tr8s logique d'utiliser un cache pour servir des donn9es, lorsque les donn9es sont une ressource fr9quemment demand9e et/ou lorsque l'interrogation de ces donn9es dans la base de donn9es est une t2che lourde et peut prendre plusieurs secondes.

Vous pouvez stocker ce type de donn9es dans votre cache et les servir 0 partir de l0 au lieu d'aller dans la base de donn9es chaque fois pour interroger les donn9es.

Une chose importante 0 garder 0 l'esprit lors de la fourniture de donn9es 0 partir d'un cache est que ces donn9es peuvent devenir obsol8tes. Vous devez donc vous assurer que les donn9es dans le cache sont toujours 0 jour.

Il existe de nombreuses solutions diff9rentes. Un exemple appropri9 est d'utiliser redis ou le middleware express apicache.

J'aimerais utiliser apicache, mais si vous souhaitez utiliser Redis, je peux fortement recommander de consulter leur excellente documentation.

Prenons un instant pour r9fl9chir 0 un sc9nario dans notre API o9 un cache serait judicieux. Je pense que la requate pour recevoir tous les entraenements serait efficacement servie 0 partir de notre cache.

Tout d'abord, installons notre middleware :

npm i apicache

Maintenant, nous devons l'importer dans notre routeur d'entraenement et le configurer.

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

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

// *** AJOUTER ***
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;

Commencer est assez simple, n'est-ce pas ? Nous pouvons d9finir un nouveau cache en appelant apicache.middleware et l'utiliser comme un middleware dans notre route get. Vous devez simplement le mettre en tant que param8tre entre le chemin r9el et notre contr4leur d'entraenement.

L0-dedans, vous pouvez d9finir combien de temps vos donn9es doivent atre mises en cache. Pour les besoins de ce tutoriel, j'ai choisi deux minutes. Le temps d9pend de la rapidit9 ou de la fr9quence de changement de vos donn9es dans votre cache.

Testons les choses !

Dans Postman ou un autre client HTTP de votre choix, d9finissez une nouvelle requate qui obtient tous les entraenements. Je l'ai fait dans le navigateur jusqu'0 pr9sent, mais je souhaite mieux visualiser les temps de r9ponse pour vous. C'est la raison pour laquelle je demande la ressource via Postman pour l'instant.

Appelons notre requate pour la premi8re fois :

Image

Comme vous pouvez le voir, notre API a mis 22,93 ms pour r9pondre. Une fois que notre cache est vide 0 nouveau (apr8s deux minutes), il doit atre rempli 0 nouveau. Cela se produit avec notre premi8re requate.

Donc, dans le cas ci-dessus, les donn9es n'ont PAS 9t9 servies depuis notre cache. Elles ont pris le chemin "r9gulier" depuis la base de donn9es et rempli notre cache.

Maintenant, avec notre deuxi8me requate, nous recevons un temps de r9ponse plus court, car elle a 9t9 servie directement depuis le cache :

Image

Nous avons 9t9 en mesure de servir trois fois plus vite que dans notre requate pr9c9dente ! Tout cela gr2ce 0 notre cache.

Dans notre exemple, nous avons mis en cache une seule route, mais vous pouvez 9galement mettre en cache toutes les routes en l'impl9mentant comme ceci :

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

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

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

app.listen(PORT, () => {
  console.log(`L'API 9coute sur le port ${PORT}`);
});

Il y a une chose importante que je voudrais noter ici en ce qui concerne la mise en cache. Bien qu'elle semble r9soudre de nombreux probl8mes pour vous, elle peut 9galement apporter certains probl8mes dans votre application.

Quelques points 0 garder 0 l'esprit lors de l'utilisation d'un cache :

  • vous devez toujours vous assurer que les donn9es dans le cache sont 0 jour car vous ne voulez pas servir des donn9es obsol8tes
  • pendant que la premi8re requate est en cours de traitement et que le cache est sur le point d'atre rempli et que d'autres requates arrivent, vous devez d9cider si vous retardez ces autres requates et servez les donn9es depuis le cache ou si elles re7oivent 9galement des donn9es directement depuis la base de donn9es comme la premi8re requate
  • c'est un autre composant dans votre infrastructure si vous choisissez un cache distribu9 comme Redis (donc vous devez vous demander si cela vaut vraiment la peine de l'utiliser)

Voici comment proc9der habituellement :

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

Lorsque je commence 0 construire une API et qu'il n'y a pas de raisons particuli8res d'utiliser un cache tout de suite, je le laisse de c4t9 et je vois ce qui se passe avec le temps. Lorsque des raisons d'utiliser un cache se pr9sentent, je peux l'impl9menter 0 ce moment-l0.

Bonnes pratiques de s9curit9

Waouh ! Cela a 9t9 un voyage assez formidable jusqu'0 pr9sent. Nous avons abord9 de nombreux points importants et 9tendu notre API en cons9quence.

Nous avons parl9 des meilleures pratiques pour augmenter l'utilisabilit9 et la performance de notre API. La s9curit9 est 9galement un facteur cl9 pour les API. Vous pouvez construire la meilleure API, mais si elle est un logiciel vuln9rable en cours d'ex9cution sur un serveur, elle devient inutile et dangereuse.

La premi8re chose et un must absolu 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 o9 des donn9es priv9es sont envoy9es entre le client et notre API.

Si vous avez des ressources qui ne devraient atre disponibles que pour les utilisateurs authentifi9s, vous devriez les prot9ger avec une v9rification d'authentification.

Dans Express, par exemple, vous pouvez l'impl9menter en tant que middleware comme nous l'avons fait avec notre cache pour des routes sp9cifiques et v9rifier d'abord si la requate est authentifi9e avant qu'elle n'acc8de 0 une ressource.

Il peut 9galement y avoir des ressources ou des interactions avec notre API que nous ne voulons pas permettre 0 tous les utilisateurs de demander. Vous devriez alors mettre en place un syst8me de r4les pour vos utilisateurs. Vous devez donc ajouter une autre logique de v9rification 0 cette route et valider si l'utilisateur a le privil8ge d'acc9der 0 cette ressource.

Les r4les d'utilisateur auraient 9galement du sens dans notre cas d'utilisation lorsque nous voulons que seuls des utilisateurs sp9cifiques (comme les entraeneurs) cr9ent, mettent 0 jour et suppriment nos entraenements et records. La lecture peut atre pour tout le monde (y compris les membres "r9guliers").

Cela peut atre g9r9 dans un autre middleware que nous utilisons pour les routes que nous souhaitons prot9ger. Par exemple, notre requate POST vers /api/v1/workouts pour cr9er un nouvel entraenement.

Dans le premier middleware, nous v9rifierons si l'utilisateur est authentifi9. Si c'est vrai, nous passerons au middleware suivant, qui serait celui pour v9rifier le r4le de l'utilisateur. Si l'utilisateur a le r4le appropri9 pour acc9der 0 cette ressource, la requate est transmise au contr4leur correspondant.

Dans le gestionnaire de route, cela ressemblerait 0 ceci :

// Dans src/v1/routes/workoutRoutes.js
...

// Middlewares personnalis9s
const authenticate = require("../../middlewares/authenticate");
const authorize = require("../../middlewares/authorize");

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

...

Pour lire davantage et obtenir d'autres bonnes pratiques sur ce sujet, je peux sugg9rer de lire cet article.

Documenter correctement votre API

Je sais que la documentation n'est d9finitivement pas une t2che pr9f9r9e des d9veloppeurs, mais c'est une chose n9cessaire 0 faire. Surtout lorsqu'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 v9rit9 dans cette d9claration car si une API n'est pas bien document9e, elle ne peut pas atre utilis9e correctement et devient donc inutile. La documentation aide 9galement 0 rendre la vie des d9veloppeurs beaucoup plus facile.

N'oubliez jamais que la documentation est g9n9ralement la premi8re interaction que les consommateurs ont avec votre API. Plus les utilisateurs peuvent comprendre rapidement la documentation, plus ils peuvent utiliser rapidement l'API.

C'est donc notre travail d'impl9menter une bonne et pr9cise documentation. Il existe de nombreux outils qui facilitent notre vie.

Comme dans d'autres domaines de l'informatique, il existe 9galement une sorte de standard pour documenter les API appel9 Sp9cification OpenAPI.

Voyons comment nous pouvons cr9er une documentation qui justifie cette sp9cification. Nous utiliserons les packages swagger-ui-express et swagger-jsdoc pour y parvenir. Vous serez 9merveill9 de voir 0 quel point c'est g9nial dans un instant !

Tout d'abord, nous configurons notre structure de base pour notre documentation. Comme nous avons pr9vu d'avoir diff9rentes versions de notre API, les docs seront 9galement un peu diff9rentes. C'est la raison pour laquelle j'aimerais d9finir notre fichier swagger pour lancer notre documentation dans le dossier de version correspondant.

# Installer les packages npm requis 
npm i swagger-jsdoc swagger-ui-express 

# Cr9er un nouveau fichier pour configurer les docs swagger 
touch src/v1/swagger.js
// Dans src/v1/swagger.js
const swaggerJSDoc = require("swagger-jsdoc");
const swaggerUi = require("swagger-ui-express");

// Informations m9ta de base sur notre 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 au format JSON
const swaggerSpec = swaggerJSDoc(options);

// Fonction pour configurer nos docs
const swaggerDocs = (app, port) => {
  // Gestionnaire de route pour visiter nos docs
  app.use("/api/v1/docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec));
  // Rendre nos docs disponibles au format JSON
  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 };

La configuration est donc assez simple. Nous avons d9fini quelques m9tadonn9es de base de notre API, cr99 les docs au format JSON et cr99 une fonction qui rend nos docs disponibles.

Pour v9rifier si tout est en ordre, nous enregistrons un simple message dans la console o9 nous pouvons trouver nos docs.

Ce sera la fonction que nous utiliserons dans notre fichier racine, o9 nous avons cr99 le serveur Express pour nous assurer que les docs sont 9galement lanc9es.

// Dans src/index.js
const express = require("express");
const bodyParser = require("body-parser");
const v1WorkoutRouter = require("./v1/routes/workoutRoutes");
// *** AJOUTER ***
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(`L'API 9coute sur le port ${PORT}`);
  /// *** AJOUTER ***
  V1SwaggerDocs(app, PORT);
});

Maintenant, vous devriez voir dans votre terminal o9 votre serveur de d9veloppement est en cours d'ex9cution :

Image

Et lorsque vous visitez localhost:3000/api/v1/docs, vous devriez d9j0 voir notre page de docs :

Image

Je suis 9merveill9 chaque fois de voir 0 quel point cela fonctionne bien. Maintenant, la structure de base est configur9e et nous pouvons commencer 0 impl9menter les docs pour nos endpoints. C'est parti !

Lorsque vous regardez options.apis dans notre fichier swagger.js, vous verrez que nous avons inclus le chemin vers nos routes d'entraenement et vers le fichier d'entraenement dans notre dossier de base de donn9es. C'est la chose la plus importante dans la configuration qui rendra toute la magie possible.

Avoir ces fichiers d9finis dans nos options swagger nous permettra d'utiliser des commentaires qui r9f9rencent OpenAPI et ont une syntaxe comme dans les fichiers yaml, qui sont n9cessaires pour configurer nos docs.

Maintenant, nous sommes prats 0 cr9er des docs pour notre premier endpoint ! Sautons directement dedans.

// Dans 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 essentiellement toute la magie pour ajouter un endpoint 0 nos docs swagger. Vous pouvez consulter toutes les sp9cifications pour d9crire un endpoint dans leur excellente documentation.

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

Image

Cela devrait vous sembler tr8s familier si vous avez d9j0 travaill9 avec des API qui ont une documentation OpenAPI. C'est la vue o9 tous nos endpoints seront list9s et vous pouvez 9tendre chacun pour obtenir plus d'informations 0 son sujet.

Image

Lorsque vous regardez de pr8s notre r9ponse, vous verrez que nous n'avons pas d9fini la valeur de retour correcte car nous disons simplement que notre propri9t9 "data" sera un tableau d'objets vides.

C'est l0 que les sch9mas entrent en jeu.

// Dans 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 cr99 notre premier sch9ma. Typiquement, cette d9finition sera dans votre fichier de sch9ma ou de mod8le o9 vous avez d9fini vos mod8les de base de donn9es.

Comme vous pouvez le voir, c'est 9galement assez simple. Nous avons d9fini toutes les propri9t9s qui composent un entraenement, y compris le type et un exemple.

Vous pouvez visiter notre page de docs 0 nouveau et nous recevrons une autre section contenant nos sch9mas.

Image

Ce sch9ma peut maintenant atre r9f9renc9 dans notre r9ponse de notre endpoint.

// Dans 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 de pr8s le bas de notre commentaire sous "items". Nous utilisons "$ref" pour cr9er une r9f9rence et r9f9rencer le chemin vers notre sch9ma que nous avons d9fini dans notre fichier d'entraenement.

Maintenant, nous sommes en mesure de montrer un Workout complet dans notre r9ponse.

Image

Assez cool, n'est-ce pas ? Vous pourriez penser "taper ces commentaires 0 la main peut atre une t2che fastidieuse".

Cela pourrait atre vrai, mais pensez-y de cette mani8re. Ces commentaires qui sont dans votre base de code sont 9galement une excellente documentation pour vous-mame en tant que d9veloppeur d'API. Vous n'avez pas besoin de visiter les docs tout le temps lorsque vous voulez connaetre la documentation d'un endpoint sp9cifique. Vous pouvez simplement le consulter en un seul endroit dans votre code source.

Documenter les endpoints aide 9galement 0 mieux les comprendre et vous "force" 0 penser 0 tout ce que vous auriez pu oublier d'impl9menter.

Comme vous pouvez le voir, j'ai effectivement oubli9 quelque chose. Les r9ponses d'erreur possibles et les param8tres de requate sont encore manquants !

Corrigeons cela :

// Dans 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 le haut de notre commentaire sous "tags", vous pouvez voir que j'ai ajout9 une autre cl9 appel9e "parameters", o9 j'ai d9fini notre param8tre de requate pour le filtrage.

Nos docs affichent maintenant cela correctement :

Image

Et pour documenter un cas d'erreur possible, nous ne renvoyons qu'une erreur 5XX 0 ce stade. Donc sous "responses", vous pouvez voir que j'ai 9galement d9fini une autre documentation pour cela.

Sur notre page de docs, cela ressemble 0 ceci :

Image

G9nial ! Nous venons de cr9er la documentation compl8te pour un endpoint. Je vous recommande vivement d'impl9menter le reste des endpoints par vous-mame pour vous familiariser avec cela. Vous apprendrez beaucoup dans le processus !

Comme vous l'avez peut-atre vu, documenter votre API ne doit pas toujours atre un casse-tate. Je pense que les outils que je vous ai pr9sent9s r9duisent votre effort global, et la configuration est assez simple.

Nous pouvons donc nous concentrer sur l'essentiel, la documentation elle-mame. 0 mon avis, la documentation de swagger/OpenAPI est tr8s bonne et il existe de nombreux excellents exemples sur Internet.

Ne pas avoir de documentation en raison de trop de travail "suppl9mentaire" ne devrait plus atre une raison.

Conclusion

Pfiou, c'9tait un voyage assez amusant. J'ai vraiment appr9ci9 9crire cet article pour vous et j'ai 9galement beaucoup appr9s.

Il peut y avoir des bonnes pratiques qui sont importantes tandis que d'autres peuvent ne pas sembler s'appliquer 0 votre situation actuelle. C'est bien, car comme je l'ai dit pr9c9demment, c'est la responsabilit9 de chaque ing9nieur de choisir les bonnes pratiques qui peuvent atre appliqu9es 0 leur situation actuelle.

J'ai fait de mon mieux pour fusionner toutes ces bonnes pratiques que j'ai r9alis9es jusqu'0 pr9sent tout en construisant notre propre API en cours de route. Cela a rendu cela tr8s amusant pour moi !

J'adorerais recevoir des retours de toute sorte. Si vous avez quelque chose 0 me dire (bon ou mauvais), n'h9sitez pas 0 me contacter :

Voici mon Instagram (vous pouvez 9galement suivre mon parcours en tant que d9veloppeur logiciel)

0 la prochaine !