a) Introduction aux RESTful API

Introduction aux RESTful API & conventions

C'est quoi une application REST ?

YoutubeImage

REST vient de REpresentational State Transfer : c'est un style architectural pour construire des applications web extensibles, où les client et serveurs sont séparés.

Dans une application REST, une interface uniforme (ou API) est définie afin de permettre à des applications de coopérer.

Toute application REST se doit d'être stateless : il n'y a pas d'enregistrement du contexte d'une session sur le serveur pour comprendre une requête d'un client.
Ainsi, les requêtes clientes ne dépendent pas d'un historique de requêtes, chaque requête contient tout l'information nécessaire au serveur.

Une RESTful API met à disposition des opérations sur des ressources via :

  • des URI ; il y a donc une adresse unique pur chaque ressource ;
  • des méthodes HTTP (GET, POST, DELETE, PATCH, PUT) représentant les opérations possibles ; on parle souvent d'opérations CRUD, des opérations de type Create, Read, Update ou Delete) ;
  • des représentations des ressources compréhensibles tant par les clients que les serveurs ; les ressources sont représentées par leur "Media type" : JSON, XML, HTML, TXT, JPEG... ; dans le cadre de ce cours, les ressources seront quasi toujours représentées via du JSON.

Conventions REST

Le type d'opération CRUD sur une ressource est défini via la méthode http de la requête.
Les opérations possibles sont :

  • GET = Read
  • POST = Create
  • DELETE = Delete 😉
  • PATCH / PUT = Update
    • PATCH = Update d'une ou plusieurs propriété(s) de la ressources
    • PUT = Update de toutes les propriétés de la ressources, ou création si la ressource n'existe pas

Voici un exemple d'application de ces conventions REST dans le cadre d'une RESTful API permettant de gérer des posts :

URIMéthode HTTPOpération
postsGETREAD ALL : Lire toutes les ressources de la collection
posts?userId=valueGETREAD ALL FILTERED : Lire toutes les ressources de la collection selon le filtre donné
posts/{id}GETREAD ONE : Lire la ressource identifiée
postsPOSTCREATE ONE : Créer une ressource basée sur les données de la requête
posts/{id}DELETEDELETE ONE : Effacer la ressource identifiée
posts/{id}PUTUPDATE ONE : Remplacer l'entièreté de la ressource par les données de la requête

Lors de l'ajout d'un post, si cette API est hébergée à l'URL racine https://jsonplaceholder.typicode.com/, alors nous pourrions identifier une ressource de type posts de cette façon : https://jsonplaceholder.typicode.com/posts/10.

Pour lire cette ressource, il faudrait faire une requête http de type GET sur cette URL : https://jsonplaceholder.typicode.com/posts/10.

Démarrage d'une RESTful API en Express & TS

Création d'un projet à partir d'un boilerplate

Nous allons maintenant découvrir notre toute première RESTful API permettant de gérer les données associées à une pizzeria, afin de bénéficier d'opérations sur des ressources de type "pizzas" et de type "drinks".

Dans votre repo web2, veuillez créer le répertoire /tutorials/back/api.

Veuillez ouvrir un terminal au niveau de ce répertoire.

Dans ce répertoire, veuillez générer une application express nommée basic sur base du boilerplate : basic-ts-api-boilerplate [R.51].
Pour ce faire :

bash
git clone https://github.com/e-vinci/basic-ts-api-boilerplate basic

Veuillez installer les dépendances :

bash
cd basic
npm i

⚡ Comme vous avez fait un clone du boilerplate, attention au Git dans le Git, n'oubliez pas de supprimer le dossier .git présent dans votre nouveau projet.

Veuillez jeter un oeil à la structure du projet basic.

Nous développons des RESTful API qui ne possèdent pas de serveur de fichiers statiques.
On n'a donc pas besoin d'avoir un répertoire /public ni d'un serveur statique (express.static).

Fonctionnement d'une application Express

Nous allons maintenant nous attarder à comprendre les concepts associés à l'utilisation d'Express, mais en focalisant sur ceux utiles aux applications REST.
Voici comment une requête faite à une application Express est traitée :

GatsbyImage
Flux d'une requête vers une application Express [R.51]

Dans ce flux de traitement d'une requête, la responsabilité des développeurs est de s'occuper de la partie "Middleware".

La grande majorité du code écrit sera du "routing middleware" : notre code s'occupera de répondre à des requêtes clientes pour différentes URLs et méthodes HTTP (GET, POST...).

On l'a déjà vu, la configuration d'une application Express, comme toutes applications Node.js, est faite au sein de package.json.

En fonction de comment est configuré l'application, on la démarrera via npm start, npm run dev, npm run build...

Un serveur web intégré à nos applications Express est démarré au sein du fichier bin/www.ts.

C'est ce fichier que vous devez mettre à jour si par exemple vous souhaitez que votre application fonctionne sur un port différent que le port par défaut 3000.

Un serveur intégré est différent d'une application web offerte par un serveur standalone comme Apache, Tomcat... C'est un serveur très léger dédié à votre application.

Les fonctions middleware en Express

C'est quoi une fonction middleware ?

Les fonctions middleware s'occupent du traitement des requêtes des clients et de la préparation des réponses :

GatsbyImage
Les fonctions Middleware et Express [R.52]

Une fonction middleware a accès aux objets de la requête et de la réponse et peut utiliser la requête et la réponse pour ajouter, par exemple, un log, pour autoriser un utilisateur, pour parser des données Json vers des objets TS/JS, pour servir des fichiers statiques, pour faire un traitement pour une route bien spécifique...

Par exemple, lors d'une opération d'ajout d'une pizza, si une fonction middleware ne termine pas le cycle de requête-réponse, elle doit appeler next() pour permettre à d'autres fonctions qui sont dans la queue de pouvoir être exécutées.

Voici les éléments associés à l'appel d'une fonction middleware :

GatsbyImage
Une fonction middleware [R.53]

Il existe différents types de fonctions middleware ayant différents cas d'utilisation :

  • Application-level middleware : la fonction middleware est liée à l'objet app et peut s'appliquer à toutes les requêtes.
  • Router-level middleware : la fonction middleware est liée à un objet de type express.router() et est très similaire au "application-level middleware", mais ne s'applique qu'à un groupe de requêtes.
  • Error-handling middleware : fonction de gestion des erreurs qui se définit comme les fonctions ci-dessus (au niveau app ou router), mais qui contient un quatrième paramètre nommé error.
  • Built-in middleware : fonctions middleware mises à disposition par Express directement. En voici quelques exemples :
    • express.static : pour servir des assets statiques ;
    • express.json : pour parser le body de requêtes en JSON vers des objets JS ;
    • express.urlencoded : pour parser des requêtes dont le body est de type "urlencoded" (type par défaut des formulaires) vers des objets TS/JS.
  • Third-party middleware : fonctions mises à disposition par la communauté et installables via npm, comme par exemple la fonction middleware cookieParser.

La suite fournit quelques exemples de fonctions middleware qui seront soit plus tard rencontrées dans notre code, soit sont extraites de la documentation d'Express : Using middleware [R.54].

Application-level middleware : exemple

Voici une fonction middleware qui sera exécutée à chaque fois qu'il y a une requête, quelque soit le chemin (ou path) associé à la requête :

js
import express from "express";
const app = express();
app.use((_req, _res, next) => {
console.log(
"Time:",
new Date().toLocaleString("fr-FR", { timeZone: "Europe/Brussels" })
);
next();
});

Router-level middleware : exemple

Voici une partie du code qui pourrait se trouver au sein d'un router de pizzas, dans un fichier /routes/pizzas.ts :

js
import { Router } from "express";
const router = Router();
router.use((_req, _res, next) => {
console.log(
"Time:",
new Date().toLocaleString("fr-FR", { timeZone: "Europe/Brussels" })
);
next();
});
router.get("/", (req, res) => {
return res.json(pizzas);
});

La première fonction middleware ne contient pas de méthode HTTP, ni de chemin, elle s'appliquerait donc à toutes les routes associées au router de pizzas.
Voici le code qui permettrait, dans /app.ts, d'appeler le router de pizzas :

js
import pizzaRouter from "./routes/pizzas";
app.use('/pizzas', pizzaRouter);

Lors de l'opération de lecture de toutes les pizzas, si le router est utilisé de cette façon, en relisant l'avant-dernier snippet, on voit que :

  • la première fonction (où il y a un console.log) s'applique donc à toutes les routes qui commencent par /pizzas ;
  • la deuxième fonction middleware s'appliquent seulement aux requêtes de type GET sur la route (ou le chemin) /pizzas (équivalent de la route /pizzas/).

Error-handling middleware : exemple

Ce type de middleware est à définir après tous les middlewares pouvant générer une erreur et est appelé via throw ou via next(err) dans une fonction middleware où un souci est détecté.

Voici la définition d'un gestionnaire d'erreurs :

js
const errorHandler: ErrorRequestHandler = (err, _req, res, _next) => {
console.error(err.stack);
return res.status(500).send("Something broke!");
};
app.use(errorHandler);

Veuillez l'ajouter dans /app.ts juste avant l'export d'app. N'oubliez pas d'importer ErrorRequestHandler depuis express.

Attention, il y a bien 4 paramètres au lieu des 3 habituels pour les autres types de fonctions middleware.

Veuillez démarrer l'API (par défaut elle est configurée sur le port 3000 au sein de bin/www.ts) :

bash
npm run dev

Nous allons maintenant simuler une erreur dans notre application pour vérifier que notre gestionnaire d'erreurs fonctionne.

Veuillez ajouter le code suivant dans /routes/pizzas.ts, comme première route, juste après defaultPizzas :

ts
router.get("/error", (_req, _res, _next) => {
throw new Error("This is an error");
// equivalent of next(new Error("This is an error"));
});

Pour redémarrer l'API, cela se fait automatiquement pour vous à chaque sauvegarde d'un fichier :

  • 👍 Nous vous conseillons de mettre l'auto-save dans VS Code : File > Auto Save.
  • Sinon, vous devez penser à faire des sauvegardes manuelles : CTRL s à chaque modification de fichier.

Pour faire une requête à cette route, veuillez taper dans votre navigateur (ou cliquez sur ce lien) : http://localhost:3000/pizzas/error.

Regardez le message d'erreur tant dans le terminal de VS Code que ce qui est affiché dans votre browser.

Built-in middleware & third-party middleware : exemple

Dans app.ts, on peut trouver pas mal d'exemples de ces types de middleware. Ils sont commentés ci-dessous dans le code :

js
const app = express();
app.use(logger('dev')); // HTTP request logger linked to morgan package
app.use(express.json()); // Parse requests with JSON payloads
app.use(express.urlencoded({ extended: false })); // Parse requests with URL-
// encoded payload
app.use(cookieParser()); // Parse cookie header (req.cookies)
app.use(express.static(path.join(__dirname, 'public'))); // Serve static assets

Définition d'une route en Express

Définition d'une route

Le routing, ou routage, contrôle la réponse à une requête client pour un chemin et une méthode HTTP. Le chemin est aussi appelé endpoint ou URI ou PATH.

On va définir une route soit sur l'objet app, soit sur un router.
Un objet de type router permet de regrouper toutes les routes associées à un type de ressources.

On définit une route de cette façon : app. ou router. METHOD(PATH, MIDDLEWARE_FUNCTION).

👍 Dans notre cours, nous vous recommandons d'organiser vos routes par type de ressources et donc de mettre en place des routers.

Opérations de lecture

Nous souhaitons découvrir comment mettre en place une opération permettant de lire toutes les ressources de type "drinks".

Pour cela, il nous faut créer un router pour traiter des ressources /drinks au sein de /routes/drinks.ts.

Pour être sûr de ne pas avoir de problème de compilation avec TypeScript, il est recommandé de toujours ouvrir votre projet en tant que workspace dans VS Code (File > Open Folder... et sélection de votre répertoire basic).

Pour l'opération de lecture de toutes les boissons, selon les conventions REST, il faut faire une requête de type GET /drinks. Le router de /routes/drinks.ts doit donc offrir une route renvoyant toutes les boissons qui existent.

Pour démarrer, nous souhaitons une application basique qui ne gère pas la persistance des données. Le menu sera donc un array d'objets, chaque objet représentant une boisson.

Notre opération de lecture de boissons va renvoyer du JSON au client, c'est à dire une représentation textuelle d'un array d'objets. Nous verrons plus tard ce qu'est réellement le JSON. A ce stade-ci, il est suffisant de connaître la fonction d'Express qui permet à un objet TS/JS de circuler sur le réseau : res.json().

Voici le code du router /routes/drinks.ts :

ts
import { Router } from "express";
import { Drink } from "../types";
const drinks: Drink[] = [
{
id: 1,
title: "Coca-Cola",
image:
"https://media.istockphoto.com/id/1289738725/fr/photo/bouteille-en-plastique-de-coke-avec-la-conception-et-le-chapeau-rouges-d%C3%A9tiquette.jpg?s=1024x1024&w=is&k=20&c=HBWfROrGDTIgD6fuvTlUq6SrwWqIC35-gceDSJ8TTP8=",
volume: 0.33,
price: 2.5,
},
{
id: 2,
title: "Pepsi",
image:
"https://media.istockphoto.com/id/185268840/fr/photo/bouteille-de-cola-sur-un-fond-blanc.jpg?s=1024x1024&w=is&k=20&c=xdsxwb4bLjzuQbkT_XvVLyBZyW36GD97T1PCW0MZ4vg=",
volume: 0.33,
price: 2.5,
},
{
id: 3,
title: "Eau Minérale",
image:
"https://media.istockphoto.com/id/1397515626/fr/photo/verre-deau-gazeuse-%C3%A0-boire-isol%C3%A9.jpg?s=1024x1024&w=is&k=20&c=iEjq6OL86Li4eDG5YGO59d1O3Ga1iMVc_Kj5oeIfAqk=",
volume: 0.5,
price: 1.5,
},
{
id: 4,
title: "Jus d'Orange",
image:
"https://images.unsplash.com/photo-1600271886742-f049cd451bba?q=80&w=1374&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
volume: 0.25,
price: 4.5,
},
{
id: 5,
title: "Limonade",
image:
"https://images.unsplash.com/photo-1583064313642-a7c149480c7e?q=80&w=1430&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
volume: 0.33,
price: 5,
},
];
const router = Router();
router.get("/", (_req, res) => {
return res.json(drinks);
});
export default router;

Ici, on a défini un array de boissons, chaque boisson étant un objet de type Drink. TS aurait pu nous aider à définir automatiquement le type de chaque objet grâce à l'inférence de type : (le 1er objet contient un id qui est un number, un title qui est une string, image qui est une string, ...).

Néanmoins, il est bien plus clair de préciser le type de chaque objet comme étant de type Drink. Une bonne pratique est de définir les types de données dans un fichier /types.ts ; veuillez mettre à jour ce fichier avec le type Drink :

ts
interface Drink {
id: number;
title: string;
image: string;
volume: number;
price: number;
}
export type { Pizza, NewPizza, PizzaToUpdate, Drink };

Et voici le code de app.ts (les parties modifiées sont surlignées) :

js
1
import express, { ErrorRequestHandler } from "express";
2
3
import usersRouter from "./routes/users";
4
import pizzaRouter from "./routes/pizzas";
5
import drinkRouter from "./routes/drinks";
6
7
const app = express();
8
9
app.use((_req, _res, next) => {
10
console.log(
11
"Time:",
12
new Date().toLocaleString("fr-FR", { timeZone: "Europe/Brussels" })
13
);
14
next();
15
});
16
17
app.use(express.json());
18
app.use(express.urlencoded({ extended: false }));
19
20
app.use("/users", usersRouter);
21
app.use("/pizzas", pizzaRouter);
22
app.use("/drinks", drinkRouter);
23
24
const errorHandler: ErrorRequestHandler = (err, _req, res, _next) => {
25
console.error(err.stack);
26
return res.status(500).send("Something broke!");
27
};
28
29
app.use(errorHandler);
30
export default app;

Pour consommer l'opération de lecture via un browser, nous pouvons lire toutes les ressources de type "drinks" ici : http://localhost:3000/drinks

Exercice 1.1 : lecture de toutes les ressources

Vous allez créer la première version de la RESTful API de myMovies, un site qui permettra de présenter des films. Vous devez, sous Express, mettre à disposition cette opération :

URIMéthode HTTPOpération
filmsGETREAD ALL : Lire toutes les ressources de la collection

Une ressource de type films doit contenir les propriétés suivantes :

  • id : un entier
  • title : titre du film (String)
  • director : le réalisateur du film (String)
  • duration : durée du film en minutes ; elle doit être un nombre positif (pas une string !).

Un film pourra avoir des propriétés supplémentaires (elles sont optionnelles) :

  • budget : pour informer du coût qu'a couté la production du film, en millions ; le budget doit être un nombre positif (pas une string !).
  • description : pour donner une URL vers une image du film.
  • imageUrl : pour donner une URL vers une image du film.

Veuillez donc "hardcoder" au moins trois ressources, parmi vos films préférés, dans un array au sein de votre RESTful API. Et si vous voulez aller très vite, demander à une IA de générez des films contenant les propriétés demandées.

Le code de votre application doit se trouver dans votre repo git au sein du répertoire /exercises/1.1. Vous allez donc créer un nouveau projet sur base d'un clone du boilerplate : basic-ts-api-boilerplate.

Veuillez supprimer tout le code du boilerplate qui n'est pas nécessaire pour cet exercice.

Une fois tout fonctionnel, veuillez faire un commit de votre code avec le message suivant : new: ex1.1.

Exercice 1.2 : middleware s'exécutant sur toutes les routes

Application middleware de base

Veuillez créer un middleware qui permette d'enregistrer et d'afficher dans la console des statistiques sur les requêtes faites à votre API.

Vous devez enregistrer, depuis le démarrage du serveur, le nombre de requêtes GET faites à votre API.

Veuillez repartir du code de la solution de votre Exercice 1.1 en créant un nouveau projet dans votre repo git dans /exercises/1.2.

Voici un example de ce qui devrait être affiché dans la console à chaque requête vers votre API :

bash
GET counter : 2

Veuillez faire un commit de votre code avec le message suivant : new: ex1.2.

🤝 Tips

  • Comment récupérer la méthode HTTP ? req.method ...
  • Comment appliquer un middleware à toutes les routes ? revoir les application-level middleware...

🍬 Challenge optionnel : et si on allait un peu plus loin ?

S'il vous reste du temps et que vous souhaitez travailler les structures de données, vous pourriez maintenant catégoriser le nombre d'appels par PATH et par méthode HTTP.

Voici un example de ce qui pourrait être affiché dans la console à chaque requête vers votre API :

bash
Request counter :
- GET / : 10
- GET /pizzas : 2
- POST /pizzas : 5
- DELETE /pizzas : 2

Utilisation du linter et du formatter pour TS

Le linter

Un linter est un outil qui analyse le code source pour signaler des erreurs de programmation, des bogues, des erreurs stylistiques et des constructions suspectes.

Pour bénéficier de feedback sur votre code lors de son écriture, vous devez avoir installé l'extension ESLint au sein de VS Code.

Vous devez aussi avoir ouvert le projet comme Workspace dans VS Code : File, Open Folder.... Le fichier de configuration de TypeScript (qui spécifie les options de compilation pour le compilateur TypeScript tsc) doit se trouver à la racine de votre Workspace.

Pour info, la configuration des règles de ESLint se fait dans le fichier .eslintrc devant se trouver à la racine d'un projet et offert au sein du boilerplate.

Il est possible de bénéficier d'un check du projet par le linter et de voir tous les avertissement ou erreurs en tapant cette commande dans votre projet :

bash
npm run lint

Le formatter

Un formateur de code est un outil qui permet de formater le code source de manière automatique et cohérente. Cela permet de rendre le code plus lisible et de suivre des conventions de codage.

Pour formatter votre code, vous devez avoir installé l'extension prettier au sein de VS Code.

Vous pouvez facilement formatter votre code :

  • soit en tapant Alt Shift F (Option Shift F sous MacOS);
  • soit en faisant un clic droit sur votre script, Format Document ; la première fois, il se peut que vous deviez sélectionner prettier comme formater : dans un fichier .ts, clic droit, Format Document With..., Configure Default Formatter.

Paramètres de route

Les route parameters sont des segments d'une URL qui sont utilisés pour capturer une valeur spécifiée à leur position dans l'URL. On récupère ces paramètres via l'objet req.params.

Pour notre pizzeria, nous souhaitons pouvoir lire une boisson identifiée par son id.

Nous allons donc ajouter le paramètre de route id.
En respect des conventions REST, un client devra faire ce genre de requête pour appeler cette opération : GET /drinks/2.

Pour continuer le tutoriel que nous avons initié dans le répertoire /tutorials/pizzeria/api/basic, voici la nouvelle route à ajouter dans le router /routes/drinks.ts :

ts
router.get("/:id", (req, res) => {
const id = Number(req.params.id);
const drink = drinks.find((drink) => drink.id === id);
if (!drink) {
return res.sendStatus(404);
}
return res.json(drink);
});

Pour consommer cette nouvelle opération via un browser, nous pouvons lire la ressource de type "drinks" identifiée par 2 dans le menu ainsi : http://localhost:3000/drinks/2

Le browser fait bien une requête du genre : GET /drinks/2.
Le paramètre de la route "2" est récupéré dans l'URL de la route par Express et est offert via req.params.id.

N'hésitez pas à faire une requête pour un identifiant n'existant pas dans les boissons : http://localhost:3000/drinks/666.

Paramètres de requête

Les query parameters sont des paramètres qui peuvent être ajoutés à une URL.
On récupère ces paramètres via l'objet req.query.

Pour notre pizzeria, nous souhaitons pouvoir filtrer toutes les ressources de type "boissons" n'étant pas plus cher qu'un certain budget.

En respect des conventions REST, un client devra faire ce genre de requêtes : GET /drinks/?budget-max=price ;

Il n'y a donc pas de nouvelle route à ajouter ici. En effet, ça reste une requête de type GET sur la route /drinks.

Veuillez donc mettre à jour /routes/drinks.ts pour la lecture de toutes les boissons en filtrant selon le budget maximum :

ts
router.get("/", (req, res) => {
if (!req.query["budget-max"]) {
// Cannot call req.query.budget-max as "-" is an operator
return res.json(drinks);
}
const budgetMax = Number(req.query["budget-max"]);
const filteredDrinks = drinks.filter((drink) => {
return drink.price <= budgetMax;
});
return res.json(filteredDrinks);
});

Pour consommer cette nouvelle opération via un browser, nous pouvons lire toutes les ressources de type "drinks" triées par leur titre de manière descendante : http://localhost:3000/drinks/?budget-max=3

N'hésitez pas à tester d'autres filtres.

Opération de création & parsing du body

Nous souhaitons développer une opération permettant de créer une ressource de type "drinks".
Selon les conventions REST, il faut faire une requête de type POST /drinks qui offre une représentation de la ressource à créer. La représentation utilisée est le JSON que nous verrons plus en détails plus tard.
Lors de l'ajout d'une boisson, si nous souhaitons créer une ressource dont le titre est "Virgin Tonic", l'image est "https://plus.unsplash.com/premium_photo-1668771899398-1cdd763f745e?q=80&w=1374&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", le volume est de 25cl et le prix est de 4,5€, alors la représentation de la ressource à créer sera la suivante :

json
{
"title":"Virgin Tonic",
"image":"https://plus.unsplash.com/premium_photo-1668771899398-1cdd763f745e?q=80&w=1374&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"volume":0.25,
"price":4.5
}

Selon les conventions REST, une requête de création est de type POST et contient ses paramètres au sein du body de la requête.

/routes/drinks.ts doit offrir une nouvelle route permettant d'ajouter une nouvelle boisson (au menu des boissons), qui est un array d'objets. Une nouvelle boisson doit donc être ajoutée à une variable, un array, qui est contenu dans la mémoire vive de notre machine.
Lorsque l'ajout d'une boisson a réussi, nous souhaitons renvoyer la représentation de la nouvelle ressource au client.

Ainsi, le client aura souvent accès à une nouvelle propriété, l'id de la ressource créée par l'API.

Voici une première version du code du router /routes/drinks.ts pour la nouvelle opération. Rajoutons-le dans notre application :

ts
router.post("/", (req, res) => {
const { title, image, volume, price } = req.body;
if (!title || !image || !volume || !price) {
return res.sendStatus(400);
}
const nextId =
drinks.reduce((maxId, drink) => (drink.id > maxId ? drink.id : maxId), 0) +
1;
const newDrink: Drink = {
id: nextId,
title,
image,
volume,
price,
};
drinks.push(newDrink);
return res.json(newDrink);
});

Ici, le linter ESLint nous avertit que req.body n'est pas défini, nous avons une allocation de valeurs de type any insécuritaire. En effet, title, image, volume et price peuvent être de n'importe quel type (any). Notre compilateur interdit ce genre de pratique, il est donc important de définir le type de req.body.

Pour cela, nous devons définir une interface qui représente la représentation de la ressource à créer.

Voici le code de /types.ts mis à jour :

ts
type NewDrink = Omit<Drink, "id">;
export type { Pizza, NewPizza, PizzaToUpdate, Drink, NewDrink };

Grâce à Omit, nous avons créé un nouveau type NewDrink qui est le type Drink sans la propriété id.

Concernant req.body, il faut faire ce que l'on appelle de la réduction de type (ou type narrowing). Cela consiste à vérifier que les propriétés sont bien définies et qu'elles sont du bon type, généralement à l'aide d'opérateurs (comme typeof ou instanceof, les opérateurs d'égalité, l'opérateur in ) qui réduisent dynamiquement le type de la variable.

Et voici le code de /routes/drinks.ts mis à jour en suivant les bonnes pratiques identifiées dans l'intro au TS de ce cours :

ts
1
router.post("/", (req, res) => {
2
const body: unknown = req.body;
3
if (
4
!body ||
5
typeof body !== "object" ||
6
!("title" in body) ||
7
!("image" in body) ||
8
!("volume" in body) ||
9
!("price" in body) ||
10
typeof body.title !== "string" ||
11
typeof body.image !== "string" ||
12
typeof body.volume !== "number" ||
13
typeof body.price !== "number" ||
14
!body.title.trim() ||
15
!body.image.trim() ||
16
body.volume <= 0 ||
17
body.price <= 0
18
) {
19
return res.sendStatus(400);
20
}
21
22
const { title, image, volume, price } = body as NewDrink;
23
24
const nextId =
25
drinks.reduce((maxId, drink) => (drink.id > maxId ? drink.id : maxId), 0) +
26
1;
27
28
const newDrink: Drink = {
29
id: nextId,
30
title,
31
image,
32
volume,
33
price,
34
};
35
36
drinks.push(newDrink);
37
return res.json(newDrink);
38
});

Nous avons finalement ajouté une assertion de type : grâce à as NewDrink, nous avons indiqué à TypeScript que body est de type NewDrink car nous en sommes certains (suite aux vérifications de type).

Il est à noter que la représentation de la ressource à créer est parsée dans l'objet req.body grâce à la fonction middleware express.json() appelée dans /app.ts :

js
app.use(express.json());

Il est donc important de ne pas oublier cette ligne lorsque l'on crée une RESTful API.

Bien, on se rend compte que la validation des données est très importante, mais elle est souvent répétitive. C'est pourquoi il peut être intéressant d'utiliser des librairies de validation de données comme Joi ou Yup.

OK, c'est bien, mais comment tester ce nouveau code ?

Le browser permet de facilement créer des requêtes de type GET, mais pas des requêtes de type POST...
Nous avons donc besoin d'un client léger permettant de faire des requêtes HTTP.

Client REST

Introduction

Dans le cadre de ce cours, tout comme généralement dans un environnement professionnel, nous souhaitons pouvoir développer une API indépendamment du développement d'une IHM (Interface Homme Machine, ce sont les écrans permettant d'interagir avec l'application web).

En effet, cela prendrait trop de temps de devoir développer un frontend pour tester nos API.

Nous allons donc utiliser un client léger permettant de faire des requêtes à nos API.
Il en existe de nombreux, comme REST Client [R.55] ou Postman [R.56].

REST Client

Dans le cadre de ce cours, nous utilisons REST Client [R.55] de Visual Studio Code pour tester nos API.

Pour installer REST Client au sein de VS Code, veuillez cliquer sur l'onglet Extensions.

Recherchez l'extension REST Client et cliquez sur Install.

Quelques notions pour utiliser REST Client :

  • Il faut créer un fichier .http (ou .rest) contenant les requêtes vers vos RESTful APIs.
    NB : Il est approprié de créer un fichier par type de ressources.
  • Chaque requête est introduite par ### (3 "#"" ou plus) ; voici la requête permettant de lire toutes les boissons :
http
### Read all drinks
GET http://localhost:3000/drinks
  • Pour exécuter une requête, il suffit de cliquer sur Send Request.
  • Lorsqu'on envoie des données au format JSON, il est important d'avoir un espace avant les accolades (avant le "{" ).
  • On peut définir des File variables via ce genre de syntaxe : @baseUrl = http://localhost:3000.
  • Pour utiliser la variable baseUrl, il suffit de la mettre entre double accolades. Par exemple, voici la requête permettant de lire toutes les boissons :
http
### Read all drinks with File variable
@baseUrl = http://localhost:3000
GET {{baseUrl}}/drinks

Nous allons maintenant tester l'API de la pizzeria que nous avons créée pour toutes ses opérations.

Au sein de VS Code, dans votre projet /tutorials/pizzeria/api/basic, veuillez créer un répertoire nommé REST Client. Dans ce répertoire, veuillez créer un fichier nommé drinks.http.

Dans drinks.http, veuillez ajouter cette requête pour la lecture de toutes les boissons et exécutez la :

http
### Read all drinks with File variable
@baseUrl = http://localhost:3000
GET {{baseUrl}}/drinks

Est-ce que cela fonctionne bien ? Avez vous bien démarré votre API ?
Vous devriez obtenir le même résultat que si vous accédiez à votre API à l'aide du browser.

Au sein de drinks.http, veuillez ajouter ces deux requêtes pour la lecture d'une seule boisson ou pour la lecture de toutes les boissons en les filtrant selon le budget maximum :

http
### Read a single drink
GET {{baseUrl}}/drinks/3
### Read all drinks cheaper or equal to 3 €
GET {{baseUrl}}/drinks/?budget-max=3

Veuillez exécuter ces deux requêtes.

Nous sommes prêts pour ajouter une requête appelant l'opération de création d'une boisson.

Au sein de drinks.http, veuillez ajouter cette requête pour la création d'une boisson :

http
### Create a drink
POST {{baseUrl}}/drinks
Content-Type: application/json
{
"title":"Virgin Tonic",
"image":"https://plus.unsplash.com/premium_photo-1668771899398-1cdd763f745e?q=80&w=1374&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"volume":0.25,
"price":4.5
}

On remarque qu'il est important de mettre une ligne vide avant les accolades représentant le body de la requête.

💭 Comment tester que le bon fonctionnement de l'opération de création ?

Il suffit d'exécuter l'opération de lecture de toutes les boissons 😎 ! Lors de l'ajout d'une boisson, si la nouvelle ressource apparaît, c'est qu'elle a bien été créée !
Faites le test !

Utilisation du debugger

Introduction

💭 Qui est votre meilleur ami ?

Il est possible qu'à ce stade-ci, vous ignorez une des bonnes réponses, car pour les développeurs, le debugger est leur meilleur ami !

Le debugger est toujours là pour vous, prêt à vous faire voyager pas à pas dans votre code, à vous donner des pistes dans les moments difficiles, sans imposer de solutions, il vous offre une liberté totale ! Et il acceptera toujours votre code tel qu'il est, sous réserve bien sûr que celui-ci compile. C'est exactement ce que l'on attend d'un ami 😁.

Utilisation de la configuration de debug offerte

Nous vous offrons une configuration de Debug permettant de facilement déboguer plusieurs applications au sein d'un même folder de VS Code. Cette configuration se trouve dans le fichier .vscode/launch.json.
Cette configuration est active au sein de VS Code que si elle se trouve à la racine du folder ouverte dans VS Code. Vous devez donc vous assurer que le dossier .vscode et son fichier launch.json se trouvent au bon endroit. Voici deux scénarios :

  • Si vous ouvrez un seul projet au sein de VS Code, c'est-à-dire que le folder ouvert de VS Code est le clone du boilerplate d'une API : vous ne devez pas déplacer le répertoire .vscode, tout est bien configuré.
  • Si vous ouvrez ou folder de VS Code contenant plusieurs projets, comme par exemple un repository contenant plusieurs API : vous devez déplacer .vscode à la racine du folder ouvert dans VS Code.

Si vous avez plusieurs applications au sein d'un folder de VS Code, pour déboguer une application en particulier, nous vous conseillons cette approche :

  • Ouvrez le fichier package.json de l'application à déboguer ;
  • Cliquez sur l'icône Run and Debug à gauche de l'Explorer, puis cliquez sur Start Debugging (ou cliquez juste sur F5) en vérifiant que la configuration de debugging sélectionnée est bien nommée Launch via NPM.

Notons que le nom de la configuration de debugging peut facilement être modifiée en changeant la valeur de l'attribut name dans /.vscode/launch.json.

Utilisation du debugger TS

Il existe un autre moyen de déboguer son application au sein de VS Code :

  • Vous pouvez installer l'extension TypeScript Debugger au sein de VS Code;
  • Ensuite, il vous suffit de créer une configuration de Debug : Add Configuration..., TS Debug ; sinon, vous pouvez sélectionner la configuration existante et nommée ts-node dans le boilerplate d'une API. Une fois que votre configuration est ouverte après avoir cliqué sur l'onglet de Debug, vous êtes prêt à déboguer.
  • Ouvrez le script d'entrée de votre application : /bin/www.ts.
  • Cliquez sur Start Debugging ou sur F5 en vérifiant que la configuration de debugging sélectionnée est bien nommée ts-node (ou le nom que vous auriez choisi pour la configuration de votre debugger pour TS).

Exercice 1.3 : lectures spécifiques, création & REST Client

Veuillez continuer le développement de la RESTful API de myMovies, sous Express, afin de mettre à disposition de nouvelles opérations sur des films et utiliser REST Client.

Veuillez repartir du code de la solution de votre Exercice 1.2 en créant un nouveau projet dans votre repo git dans /exercises/1.3.

Veuillez rajouter ces opérations à votre API :

URIMéthode HTTPOpération
films?minimum-duration=valueGETREAD ALL FILTERED : Lire toutes les ressources de la collection selon le filtre donné
films/{id}GETREAD ONE : Lire la ressource identifiée
filmsPOSTCREATE ONE : Créer une ressource basée sur les données de la requête

Pour rappel, une ressource de type films doit contenir les propriétés suivantes :

  • id : un entier
  • title : titre du film (String)
  • director : le réalisateur du film (String)
  • duration : durée du film en minutes ; elle doit être un nombre positif (pas une string !).

Un film pourra avoir des propriétés supplémentaires (elles sont optionnelles) :

  • budget : pour informer du coût qu'a coûté la production du film, en millions ; le budget doit être un nombre positif (pas une string !).
  • description : pour donner une URL vers une image du film.
  • imageUrl : pour donner une URL vers une image du film.

Les ressources ne doivent toujours pas persister : dès lors, ajoutez les données associées aux films dans un array.

Veuillez bien valider les paramètres reçu par les opérations de vos API ; vérifiez par exemple que budget et duration sont des nombres positifs. Si ça n'est pas le cas, renvoyer un message d'erreur (comme par exemple "Wrong minimum duration") au format JSON (via res.json). Ceci n'est pas une bonne pratique, mais pour l'instant, c'est suffisant.

Veuillez tester toutes les fonctions de la RESTful API pour la collection de films à l'aide du REST Client dans VS Code. Veuillez ajouter vos requêtes au sein du fichier films.http dans le répertoire REST Client du dossier associé à cet exercice.

Veuillez tester pas à pas chaque ligne de votre opération de création de film. Pour ce faire, vous devez utiliser le debugger !

Une fois tout fonctionnel, veuillez faire un commit de votre code avec le message suivant : new: ex1.3.

🤝 Tips

  • Développez les opérations de votre API de manière incrémentale : testez une opération via REST Client avant de passer à une nouvelle opération.
  • Pour le filtre sur les films, vous allez récupérer un paramètre de requête.
    ⚡ Attention, le signe - est un opérateur en TS/JS, vous ne pouvez pas récupérer le paramètre de requête via req.query.minimum-duration...
    💭 Mais alors comment faire ?
    On accède aussi au propriété d'un objet à l'aide d'un array, ici ça serait via req.query['minimum-duration'].
  • Pour transformer une string en nombre, vous pouvez utiliser la fonction Number : Number(req.query['minimum-duration'])...

🍬 Challenge optionnel

Si vous avez encore du temps, pour l'opération de création, vous pourriez ajouter une validation des données plus robuste en assurant qu'aucune propriété inattendue n'est présente dans la représentation de la ressource à créer.

Une fois tout fonctionnel, veuillez faire un commit de votre code avec le message suivant : new: ex1.3++.

🍬 Exercice 1.4 : Gestion de la pagination, du tri et du filtrage

N'hésitez pas, c'est optionnel, de gérer de nouvelles opérations au sein de votre RESTful API de myMovies :

  • Filtrez tous les films qui commencent par une certaines chaînes de caractères.
  • Permettez de trier les films.

Le code de votre application est à ajouter dans votre repo git dans /exercises/1.4.
Veuillez faire un commit de votre code avec le message suivant : 1.4 : API : ordering & filtering client.

🍬 Et si vraiment vous avez encore du temps et souhaitez déjà approfondir les RESTful APIs, n'hésitez pas aussi à implémenter la gestion de la pagination. Pour cette partie, veuillez faire un commit de votre code avec le message suivant : new: ex1.4.

🤝 Tips

Besoin d'inspiration pour l'aspect filtrage et la gestion du tri des ressources ? REST API Guide [R.58].

Codes de statut HTTP associés aux réponses

On ne peut pas toujours renvoyer du JSON suite à une requête client ainsi qu'un code HTTP correspondant au fait que tout est OK (200 OK).

Quand vous exécutez cette requête :

http
### Read all drinks with File variable
GET {{baseUrl}}/drinks

Vous faites appel à l'opération de lecture de toutes les boissons. La dernière ligne de cette opération est la suivante :

ts
return res.json(filteredDrinks);

La fonction json renvoie une réponse au format JSON, mais de plus, elle renvoie un status code 200 indiquant au client que tout s'est bien passé.

Au sein de drinks.http, veuillez ajouter cette requête pour tenter de créer une boisson en oubliant un paramètre :

http
### Try to create a drink with incomplete data
POST {{baseUrl}}/drinks
Content-Type: application/json
{
"title":"Missing Data Drink",
"volume":0.25,
"price":4.5
}

Veuillez exécuter cette requête. Que se passe-t-il ?
On récupère un code d'erreur 400 Bad Request.

En effet, lorsqu'on omet un paramètre dans la représentation de la ressource à créer, voici les lignes de code amenant au renvoi du code d'erreur 400 au sein de drinks.ts :

ts
1
router.post("/", (req, res) => {
2
const body: unknown = req.body;
3
if (
4
!body ||
5
typeof body !== "object" ||
6
!("title" in body) ||
7
!("image" in body) ||
8
!("volume" in body) ||
9
!("price" in body) ||
10
typeof body.title !== "string" ||
11
typeof body.image !== "string" ||
12
typeof body.volume !== "number" ||
13
typeof body.price !== "number" ||
14
!body.title.trim() ||
15
!body.image.trim() ||
16
body.volume <= 0 ||
17
body.price <= 0
18
) {
19
return res.sendStatus(400);
20
}
21
22
const { title, image, volume, price } = body as NewDrink;
23
24
const nextId =
25
drinks.reduce((maxId, drink) => (drink.id > maxId ? drink.id : maxId), 0) +
26
1;
27
28
const newDrink: Drink = {
29
id: nextId,
30
title,
31
image,
32
volume,
33
price,
34
};
35
36
drinks.push(newDrink);
37
return res.json(newDrink);
38
});

Le client est donc bien informé qu'il y a eu un problème lors de l'exécution de l'opération.
Il pourrait par exemple utiliser cette information pour présenter un message d'erreur au niveau d'une IHM.

Voici les grandes catégories de "status codes" :

  • Réponses informatives : 100-199
  • Réponses en cas de succès : 200-299
  • Redirections : 300-399
  • Erreurs du client : 400-499
  • Erreurs du serveur : 500-599

Voici les "status codes" que nous allons généralement utiliser :

  • 200 OK : tout s'est bien passé, Express ajoute ce code automatiquement pour nous quand nous utilisons une méthode comme res.json().
  • 400 Bad Request : pour indiquer au client que la requête contient des paramètres non valides ou n'est pas complète.
  • 401 Unauthorized : pour indiquer au client qu'il doit s'authentifier pour accéder à cette opération. On renvoie aussi ce code d'erreur quand un client fournit un mauvais username ou password.
  • 403 Forbidden : le client est connu du serveur, mais il n'a pas les privilèges pour accéder à cette opération (par exemple, le client n'est pas admin et tente d'accéder à une opération seulement accessible à un admin).
  • 404 Not Found : la ressource demandée n'existe pas, bien que l'URL semble valide.
  • 409 Conflict : l'état du serveur entre en conflit avec la requête. Par exemple, la requête demande de créer un utilisateur qui existe déjà.
  • 500 Internal Server Error : le serveur a rencontré une erreur qu'il ne peut pas régler. Par exemple, le serveur de base de données ne répond pas et ne permet donc pas d'accéder aux ressources.

Exercice 1.5 : codes de statut HTTP

Veuillez continuer le développement de la RESTful API de myMovies, sous Express, afin de mieux gérer la la lecture et la création de films et les réponses à donner aux clients.

Veuillez créer un nouveau projet dans votre repo git dans /exercises/1.5 en partant du code de la solution de votre Exercice 1.3 ou de votre Exercice 1.4 optionnel.

Veuillez améliorer les deux opérations de lecture (GET /films & GET /films/:id) et l'opération de création de films (POST /films):

  • En cas d'échec de la validation des paramètres reçus par une opération (non respect du contrat de l'API), veuillez renvoyer le status code approprié.
  • Lors de l'échec de la lecture d'un film en particulier, veuillez renvoyer le status code approprié.
  • Lors de l'ajout d'un film, si la ressource existe déjà, c'est-à-dire s'il y a déjà un film présent avec le title et le director donné, veuillez renvoyer le status code approprié.

Veuillez faire un commit de votre code avec le message suivant : new: ex1.5.

Opérations de suppression & de modification

Opération de suppression

Nous souhaitons développer une opération permettant de supprimer une ressource de type "boisson" à l'aide de son identifiant.

Selon les conventions REST, une opération de suppression:

  • est associée à une requête de type DELETE /drinks/{id} contenant l'identifiant de la ressource à supprimer au sein de l'URI comme paramètre de route.
  • ne contient pas de données dans le body et est de type DELETE.

Voici le code du router /routes/drinks.ts pour la nouvelle opération, veuillez la rajouter dans le répertoire de votre tutoriel en cours :

ts
router.delete("/:id", (req, res) => {
const id = Number(req.params.id);
const index = drinks.findIndex((drink) => drink.id === id);
if (index === -1) {
return res.sendStatus(404);
}
const deletedElements = drinks.splice(index, 1); // splice() returns an array of the deleted elements
return res.json(deletedElements[0]);
});

Au sein de drinks.http, veuillez ajouter cette requête pour supprimer la boisson possédant l'identifiant "2" :

http
### Delete a drink
DELETE {{baseUrl}}/drinks/2

Veuillez exécuter cette requête et vérifier que la boisson a bien été supprimée.

Opération de modification

Nous souhaitons développer une opération permettant de modifier une ressource de type "drinks" à l'aide de son identifiant et de nouvelles valeurs pour ses propriétés.

Selon les conventions REST, une opération de modification :

  • si l'on accepte de modifier que certaines des propriétés d'une boisson (que l'on ne doit donc pas fournir toutes les propriétés d'une boisson), est associée à une requête de type PATCH /drinks/{id} contenant l'identifiant de la ressource à supprimer au sein de l'URL comme paramètre de route.
  • contient les nouvelles données au sein du body et est de type PATCH ou PUT.

Lors de l'ajout d'une boisson, si nous souhaitons modifier une ressource identifiée par 5 en fournissant un nouveau titre "Citronnade", la représentation des données de la ressource à modifier sera la suivante :

json
{
"title":"Citronnade"
}

Selon les conventions REST, la requête de modification est de type PATCH et contient ses paramètres au sein du body de la requête.

Voici le code du router /routes/drinks.ts pour la nouvelle opération à rajouter dans votre tutoriel en cours :

ts
router.patch("/:id", (req, res) => {
const id = Number(req.params.id);
const drink = drinks.find((drink) => drink.id === id);
if (!drink) {
return res.sendStatus(404);
}
const body: unknown = req.body;
if (
!body ||
typeof body !== "object" ||
("title" in body &&
(typeof body.title !== "string" || !body.title.trim())) ||
("image" in body &&
(typeof body.image !== "string" || !body.image.trim())) ||
("volume" in body &&
(typeof body.volume !== "number" || body.volume <= 0)) ||
("price" in body && (typeof body.price !== "number" || body.price <= 0))
) {
return res.sendStatus(400);
}
const { title, image, volume, price }: Partial<NewDrink> = body;
if (title) {
drink.title = title;
}
if (image) {
drink.image = image;
}
if (volume) {
drink.volume = volume;
}
if (price) {
drink.price = price;
}
return res.json(drink);
});

Au sein de drinks.http, veuillez ajouter cette requête pour modifier la boisson possédant l'identifiant "5" :

http
### Update the drink identified by 5
PATCH {{baseUrl}}/drinks/5
Content-Type: application/json
{
"title":"Citronnade"
}

Veuillez exécuter cette requête et vérifier que la boisson a bien été modifiée.

En cas de souci, vous pouvez accéder au code du tutoriel ici : basic.

Exercice 1.6 : suppression & modification de ressources

Veuillez continuer le développement de la RESTful API de myMovies, sous Express, afin d'ajouter les opérations de suppression et de modification de ressources.

Veuillez créer un nouveau projet dans votre repo git dans /exercises/1.6 en repartant du code de la solution de votre Exercice 1.5.

Veuillez ajouter ces trois nouvelles opérations :

URIMéthode HTTPOpération
films/{id}DELETEDELETE ONE : Effacer la ressource identifiée
films/{id}PATCHUPDATE ONE : Mettre à jour les propriétés de la ressource par les valeurs données dans la requête, pour une ou plusieurs propriétés
films/{id}PUTUPDATE ONE or CREATE ONE : Remplacer la ressource par une ressource reprenant les valeurs données dans la requête, seulement si toutes les propriétés non optionnelles de la ressource sont données ! Si la ressource n'existe pas, créer cette ressource seulement si l'id donné n'est pas déjà existant.

Veuillez bien valider les paramètres reçus par les opérations de vos API ; vérifiez par exemple que budget et duration sont des nombres positifs.

Veuillez tester toutes les fonctions de la RESTful API pour la collection de films à l'aide du REST Client dans VS Code. Veuillez ajouter vos requêtes au sein du fichier films.http dans le répertoire REST Client du dossier associé à cet exercice.

Et n'hésitez pas à utiliser le debugger pour tester pas à pas chaque ligne de vos opérations qui ne fournissent pas le résultat attendu.

Veuillez faire un commit de votre code avec le message suivant : new: ex1.6.

🍬 Challenge optionnel

Si vous avez encore du temps, pour les opérations de modification, vous pourriez ajouter une validation des données plus robuste en assurant qu'aucune propriété inattendue n'est présente dans la représentation de la ressource à modifier (ou à créer si c'est un put).

De plus, pour l'opération PUT, lorsqu'il s'agit d'une création, vous pourriez ajouter une validation pour vérifier que le film n'existe pas déjà (sur base du title et du director).

Une fois tout fonctionnel, veuillez faire un commit de votre code avec le message suivant : new: ex1.6++.