Express 01.2bis - đ GET en dĂ©tail
â ïž Avant de commencer cette quĂȘte, nous te conseillons de terminer celle-ci :
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.
Wikipedia page
đ€Œ 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 sonid, 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 :
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) :
- https://api.github.com/users?since=100000 - Renvoie une liste d'utilisateurs (tableau d'objets).
- https://api.github.com/users/tj - Renvoie un utilisateur (objet) à l'aide de son identifiant (pour information, cet utilisateur est le créateur d'Express).
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'objetreq.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.
Un article pour clarifier la distinction entre req.query et req.params.
đż 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 :
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
sqletsqlValues. - Par défaut,
sqlcontiendraselect * from moviesetsqlValuessera un tableau vide. - Si
req.query.colorest défini,sqlcontiendraselect * from movies where color = ?etsqlValuescontiendra le paramÚtre de couleur. - Une fois que
sqletsqlValuessont prĂȘts, tu peux les passer Ădatabase.query().
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.
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.
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 ;)
à 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.
La question de StackOverflow
đȘ Challenge
GET me the users
Ajoute deux filtres pour la route GET /api/users existant :
- si un paramĂštre
languageest fourni dans l'URL (?language=English), ne renvoie que les locuteurs de cette langue. - si un paramĂštre
cityest 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
languageoucity. - [ ] (bonus) Si les paramĂštres
languageetcitysont tous les 2 fournis, les éléments renvoyés doivent répondre aux deux conditions de filtrage. - [ ] GET
api/usersrenvoie un statut 200 mĂȘme si la liste est vide.
==$==
Ce que tu devrais avoir dans :
// ...
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");
});
};
// ...
// ...
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");
});
};
// ...