Archives Wild Code School
Express.js

Express 01.2bis - 🔭 GET en dĂ©tail

QuĂȘte 3 sur 12 · 1 image archivĂ©e

⚠ Avant de commencer cette quĂȘte, nous te conseillons de terminer celle-ci :
QuĂȘtes liĂ©es

Dans les quĂȘtes prĂ©cĂ©dentes, tu as dĂ©couvert comment construire une API avec Express, en utilisant une base de donnĂ©es MySQL et comment tester tes routes avec Postman.

Tu as également créé des routes avec Express, te permettant d'effectuer différentes opérations de base de données : sélectionner (GET), insérer (POST), mettre à jour (PUT) et supprimer (DELETE).

Cette quĂȘte se concentre sur le traitement dĂ©taillĂ© des requĂȘtes GET. Cela permettra de revenir sur les objets Request (req) et Response (res) utilisĂ©s dans les HANDLERS de route Express.

đŸ€“ Dans cette quĂȘte, tu vas apprendre Ă  :

✅ DiffĂ©rencier deux types de requĂȘtes GET ✅ Comprendre comment utiliser req.params et req.query ✅ Utiliser les paramĂštres d'URL pour crĂ©er une requĂȘte SQL

đŸ€“ Rappels sur les APIs REST

Les API REST (ou RESTful) fournissent au client des ressources. Par exemple, dans les quĂȘtes Express prĂ©cĂ©dentes, tu as fourni des ressources "users" et "movies".

Chaque ressource est identifiée par une URL unique. L'URL http://fakebook.com/users/10 identifie un utilisateur avec l'id 10.

L'envoi d'une requĂȘte avec une mĂ©thode HTTP spĂ©cifique au serveur entraĂźne une opĂ©ration correspondante dans la base de donnĂ©es. C'est ce qu'on appelle le CRUD (Create-Read-Update-Delete), et tu peux trouver des Ă©quivalents ici :

Verb CRUD Signification HTTP Method SQL Query
Create Créer une ressource POST INSERT
Read Lire une ou plusieurs ressources GET SELECT
Update Mettre a jour une ressource PUT or PATCH UPDATE
Delete Supprimer une ressource DELETE DELETE

La définition de CRUD sur Wikipedia est assez concise et accessible : suis le lien ci-dessous pour en savoir plus.

↗
CRUD

Wikipedia page

wikipedia.org

đŸ€Œ Deux types de requĂȘte GET

Bien que le terme CRUD soit largement utilisĂ©, il ignore une diffĂ©rence importante entre deux types de requĂȘtes GET :

  • Une application peut avoir besoin de rĂ©cupĂ©rer un ensemble de ressources, par exemple, une liste de films. Pour cela, tu peux utiliser l'URL localhost:5000/api/movies.
  • Elle peut, au contraire, nĂ©cessiter de rĂ©cupĂ©rer une ressource, par exemple un seul film. Dans ce cas, tu pourrais ajouter un / suivi d'une valeur pour identifier prĂ©cisĂ©ment la ressource ciblĂ©e. Cela peut ĂȘtre son id, comme dans http://localhost:5000/api/movies/2.

đŸ€·đŸ»â€â™€ïž Pourquoi deux types de requĂȘtes GET?

Si tu peux récupérer une liste directement, en quoi cela t'aiderait de récupérer une seule ressource ?

L'une des motivations est de maintenir la cohérence de l'API REST.

Dans une API REST, des routes doivent exister pour permettre la mise Ă  jour ou la suppression d'une ressource. Tu reverras (ou as dĂ©jĂ  vu ?) cela dans d'autres quĂȘtes :

QuĂȘtes liĂ©es

Cela permet Ă  ton API de gĂ©rer les requĂȘtes PUT et DELETE sur /api/movies/:id. Il est logique d'avoir Ă©galement la possibilitĂ© de lire (GET) une ressource sur la mĂȘme URL.

L'autre motivation est de minimiser la quantité de données transférées sur le réseau, et donc le temps de transfert. C'est un critÚre de performance important, surtout pour les connexions lentes !

Le transfert d'une liste de ressources peut prendre beaucoup de temps, surtout s'il y a beaucoup d'éléments et/ou si chaque élément a un grand nombre de champs. Dans l'API, il est possible de remédier à ce problÚme de la maniÚre suivante :

  • En ne renvoyant qu'une partie de la liste complĂšte ("pagination").
  • Renvoyer uniquement les champs "principaux" de chaque ressource.

Le transfert d'une seule ressource est moins gourmand en bande passante. Nous pouvons donc nous permettre d'inclure plus de champs dans la réponse.

Pour illustrer cela, interroge les deux URL suivantes de l'API GitHub (via Postman ou ton navigateur) :

Tu peux remarquer les éléments suivants concernant les données renvoyées :

  • Le tableau renvoyĂ© par la premiĂšre route est limitĂ© Ă  30 profils d'utilisateurs (imagine le temps qu'il faudrait pour rĂ©cupĂ©rer des millions de profils enregistrĂ©s !)
  • L'objet retournĂ© par la deuxiĂšme route contient des champs tels que email, location, bio... absents des rĂ©sultats de la premiĂšre route.

Tu peux Ă©galement remarquer que ces deux URL sont configurables, mais pas de la mĂȘme maniĂšre :

  • Dans la premiĂšre URL, une chaĂźne est passĂ©e avec des paramĂštres d'URL ou query string (?since=100000). Elle permet de filtrer l'ensemble des rĂ©sultats. La query string ne fait pas partie du path de l'URL (https://api.github.com/users) et est gĂ©nĂ©ralement facultative. Dans Express, tu peux rĂ©cupĂ©rer ces donnĂ©es via l'objet req.query.
  • Dans la deuxiĂšme URL, l'identifiant unique de la ressource est inclus dans le path de l'URL. Dans Express, ce type de paramĂštre est rĂ©cupĂ©rĂ© via l'objet req.params.

Ces deux façons de récupérer des variables via l'URL sont documentées dans l'article ci-dessous.

↗
Distinction entre req.params et req.query

Un article pour clarifier la distinction entre req.query et req.params.

coursework.vschool.io

🍿 Retour sur l'application des films

Tu vas modifier l'application pour ajouter la possibilité de filtrer la liste des films, via la query string.

🔎 Implementer un filtre

Nous allons utiliser un paramÚtre d'URL non obligatoire (par exemple, ?color=1) pour filtrer les résultats.

Cela signifie que le mĂȘme HANDLER pourra ĂȘtre utilisĂ© de deux maniĂšres. Par dĂ©faut, la requĂȘte GET /api/movies conduira Ă  la requĂȘte SQL select * from movies, sans aucune autre valeur transmise Ă  database.query(). Ceci est couvert par notre code actuel :

javascript
const getMovies = (req, res) => {
  database
    .query("select * from movies")
    .then(([movies]) => {
      res.json(movies);
    })
    .catch((err) => {
      console.error(err);
      res.status(500).send("Error retrieving data from database");
    });
};

Si req.query.color est dĂ©fini, tu devrais modifier la requĂȘte SQL en select * from movies where color = ?, et passer la valeur de la couleur dans un tableau comme second paramĂštre Ă  database.query(). Essaie d'adapter le code pour ajouter cette fonctionnalitĂ©. Quelques conseils :

  • Tu dois d'abord dĂ©clarer des variables pour stocker la chaĂźne SQL et le tableau avec les valeurs SQL. Tu peux les appeler sql et sqlValues.
  • Par dĂ©faut, sql contiendra select * from movies et sqlValues sera un tableau vide.
  • Si req.query.color est dĂ©fini, sql contiendra select * from movies where color = ? et sqlValues contiendra le paramĂštre de couleur.
  • Une fois que sql et sqlValues sont prĂȘts, tu peux les passer Ă  database.query().
hidden
Afficher la solution|||javascript|||Tu as des doutes sur comment faire ?|||0|||Masquer
const getMovies = (req, res) => {
  let sql = "select * from movies";
  const sqlValues = [];

  if (req.query.color != null) {
    sql += " where color = ?";
    sqlValues.push(req.query.color);
  }

  database
    .query(sql, sqlValues)
    .then(([movies]) => {
      res.json(movies);
    })
    .catch((err) => {
      console.error(err);
      res.status(500).send("Error retrieving data from database");
    });
};

Maintenant que tu en es lĂ , pourquoi ne pas pimenter un peu les choses ? Essaie d'ajouter un paramĂštre max_duration pour filtrer les films avec une duration infĂ©rieure ou Ă©gale Ă  la valeur dans la query string. Fais-le Ă©tape par Ă©tape : essaie d'abord de gĂ©rer max_duration sans te soucier du paramĂštre color. Ton objectif est de rĂ©pondre Ă  des requĂȘtes telles que GET /api/movies/?max_duration=150.

hidden
Afficher la solution|||javascript|||Tu as des doutes sur comment faire ?|||0|||Masquer
let sql = "select * from movies";
const sqlValues = [];

if (req.query.color != null) {
  sql += " where color = ?";
  sqlValues.push(req.query.color);
}
if (req.query.max_duration != null) {
  sql += " where duration <= ?";
  sqlValues.push(req.query.max_duration);
}

Essaie maintenant de gĂ©rer Ă  la fois max_duration et color. Ton objectif est maintenant de rĂ©pondre Ă  des requĂȘtes telles que GET /api/movies/?max_duration=150&color=0.

hidden
Afficher la solution|||javascript|||Tu as des doutes sur comment faire ?|||0|||Masquer
let sql = "select * from movies";
const sqlValues = [];

if (req.query.color != null) {
  sql += " where color = ?";
  sqlValues.push(req.query.color);

  if (req.query.max_duration != null) {
    sql += " and duration <= ?";
    sqlValues.push(req.query.max_duration);
  }
} else if (req.query.max_duration != null) {
  sql += " where duration <= ?";
  sqlValues.push(req.query.max_duration);
}

Ça commence Ă  se compliquer. Imagine si tu dois gĂ©rer encore plus de paramĂštres : tu auras des tonnes de "if" imbriquĂ©s pour gĂ©rer tous les cas. Si tu n'as pas peur de voir du code plus avancĂ©, celui cachĂ© ci-dessous s'attaque Ă  ce problĂšme : ouvre Ă  tes risques et pĂ©rils ;)

hidden
À fond, montre moi !|||javascript|||Tu veux vraiment faire ça ?|||0|||Masquer
const getMovies = (req, res) => {
  const initialSql = "select * from movies";
  const where = [];

  if (req.query.color != null) {
    where.push({
      column: "color",
      value: req.query.color,
      operator: "=",
    });
  }
  if (req.query.max_duration != null) {
    where.push({
      column: "duration",
      value: req.query.max_duration,
      operator: "<=",
    });
  }

  database
    .query(
      where.reduce(
        (sql, { column, operator }, index) =>
          `${sql} ${index === 0 ? "where" : "and"} ${column} ${operator} ?`,
        initialSql
      ),
      where.map(({ value }) => value)
    )
    .then(([movies]) => {
      res.json(movies);
    })
    .catch((err) => {
      console.error(err);
      res.status(500).send("Error retrieving data from database");
    });
};

404 ?

A partir de lĂ , si tu GET /api/movies/?max_duration=0, tu obtiendras un tableau vide. Si tu vĂ©rifies le statut de la requĂȘte dans les outils de dĂ©veloppement du navigateur ou dans Postman, tu verras que tu obtiens un statut 200. Mais ton serveur ne devrait-il pas renvoyer un 404, comme la route pour obtenir un film alors que le film n'existe pas ?

Eh bien, pas vraiment. Bien que la question soit discutable, la grande majoritĂ© considĂšre qu'il est plus appropriĂ© de retourner un code 200 qu'un code 404 lors d'une requĂȘte qui produit un tableau vide. Une raison est donnĂ©e dans cet discussion sur StackOverflow, qui pose la question : [Une API RESTful doit-elle renvoyer un 404 pour les tableaux d'objets ?] (https://stackoverflow.com/questions/6045895/should-a-restful-api-return-404-for-arrays-of-objects).

En résumé :

  • Une ressource (au singulier) est considĂ©rĂ©e comme existante (200), ou non existante (404).
  • Mais une collection (autre nom donnĂ© Ă  une liste) existe toujours, mĂȘme si elle est vide : on retourne donc toujours le code 200.
↗
Une API RESTful doit-elle renvoyer 404 pour les tableaux d'objets ?

La question de StackOverflow

stackoverflow.com

đŸ’Ș Challenge

GET me the users

Ajoute deux filtres pour la route GET /api/users existant :

  • si un paramĂštre language est fourni dans l'URL (?language=English), ne renvoie que les locuteurs de cette langue.
  • si un paramĂštre city est fourni dans l'URL (?city=Paris), ne renvoie que les utilisateurs dont la ville correspond au paramĂštre.

Publie ton application améliorée sur GitHub et partage l'URL ici.

🧐 Critùres de validation

  • [ ] La route renvoyant une liste d'utilisateurs les renvoie tous si aucun paramĂštre n'est passĂ© via l'URL.
  • [ ] Sinon, elle retourne une liste filtrĂ©e selon le paramĂštre de requĂȘte language ou city.
  • [ ] (bonus) Si les paramĂštres language et city sont tous les 2 fournis, les Ă©lĂ©ments renvoyĂ©s doivent rĂ©pondre aux deux conditions de filtrage.
  • [ ] GET api/users renvoie un statut 200 mĂȘme si la liste est vide.

==$==

Ce que tu devrais avoir dans :

javascript
// ...

const getUsers = (req, res) => {
  const initialSql = "select * from users";
  const where = [];

  if (req.query.city != null) {
    where.push({
      column: "city",
      value: req.query.city,
      operator: "=",
    });
  }
  if (req.query.language != null) {
    where.push({
      column: "language",
      value: req.query.language,
      operator: "=",
    });
  }

  database
    .query(
      where.reduce(
        (sql, { column, operator }, index) =>
          `${sql} ${index === 0 ? "where" : "and"} ${column} ${operator} ?`,
        initialSql
      ),
      where.map(({ value }) => value)
    )
    .then(([users]) => {
      res.json(users);
    })
    .catch((err) => {
      console.error(err);
      res.status(500).send("Error retrieving data from database");
    });
};

// ...
javascript
// ...

const getMovies = (req, res) => {
  const initialSql = "select * from movies";
  const where = [];

  if (req.query.color != null) {
    where.push({
      column: "color",
      value: req.query.color,
      operator: "=",
    });
  }
  if (req.query.max_duration != null) {
    where.push({
      column: "duration",
      value: req.query.max_duration,
      operator: "<=",
    });
  }

  database
    .query(
      where.reduce(
        (sql, { column, operator }, index) =>
          `${sql} ${index === 0 ? "where" : "and"} ${column} ${operator} ?`,
        initialSql
      ),
      where.map(({ value }) => value)
    )
    .then(([movies]) => {
      res.json(movies);
    })
    .catch((err) => {
      console.error(err);
      res.status(500).send("Error retrieving data from database");
    });
};

// ...