a) Introduction aux RESTful API
Introduction aux RESTful API & conventions
C'est quoi une application REST ?
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 :
URI | Méthode HTTP | Opération |
---|---|---|
posts | GET | READ ALL : Lire toutes les ressources de la collection |
posts?userId=value | GET | READ ALL FILTERED : Lire toutes les ressources de la collection selon le filtre donné |
posts/{id} | GET | READ ONE : Lire la ressource identifiée |
posts | POST | CREATE ONE : Créer une ressource basée sur les données de la requête |
posts/{id} | DELETE | DELETE ONE : Effacer la ressource identifiée |
posts/{id} | PUT | UPDATE 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 :
bashgit clone https://github.com/e-vinci/basic-ts-api-boilerplate basic
Veuillez installer les dépendances :
bashcd basicnpm 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 :

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 :

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 :

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
ourouter
), 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 :
jsimport 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
:
jsimport { 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 :
jsimport 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 :
jsconst 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
) :
bashnpm 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
:
tsrouter.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 :
jsconst app = express();app.use(logger('dev')); // HTTP request logger linked to morgan packageapp.use(express.json()); // Parse requests with JSON payloadsapp.use(express.urlencoded({ extended: false })); // Parse requests with URL-// encoded payloadapp.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
:
tsimport { 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
:
tsinterface 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) :
js1import express, { ErrorRequestHandler } from "express";23import usersRouter from "./routes/users";4import pizzaRouter from "./routes/pizzas";5import drinkRouter from "./routes/drinks";67const app = express();89app.use((_req, _res, next) => {10console.log(11"Time:",12new Date().toLocaleString("fr-FR", { timeZone: "Europe/Brussels" })13);14next();15});1617app.use(express.json());18app.use(express.urlencoded({ extended: false }));1920app.use("/users", usersRouter);21app.use("/pizzas", pizzaRouter);22app.use("/drinks", drinkRouter);2324const errorHandler: ErrorRequestHandler = (err, _req, res, _next) => {25console.error(err.stack);26return res.status(500).send("Something broke!");27};2829app.use(errorHandler);30export 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 :
URI | Méthode HTTP | Opération |
---|---|---|
films | GET | READ ALL : Lire toutes les ressources de la collection |
Une ressource de type films
doit contenir les propriétés suivantes :
id
: un entiertitle
: 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 :
bashGET 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 :
bashRequest 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 :
bashnpm 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
:
tsrouter.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 :
tsrouter.get("/", (req, res) => {if (!req.query["budget-max"]) {// Cannot call req.query.budget-max as "-" is an operatorreturn 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 :
tsrouter.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 :
tstype 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 :
ts1router.post("/", (req, res) => {2const body: unknown = req.body;3if (4!body ||5typeof body !== "object" ||6!("title" in body) ||7!("image" in body) ||8!("volume" in body) ||9!("price" in body) ||10typeof body.title !== "string" ||11typeof body.image !== "string" ||12typeof body.volume !== "number" ||13typeof body.price !== "number" ||14!body.title.trim() ||15!body.image.trim() ||16body.volume <= 0 ||17body.price <= 018) {19return res.sendStatus(400);20}2122const { title, image, volume, price } = body as NewDrink;2324const nextId =25drinks.reduce((maxId, drink) => (drink.id > maxId ? drink.id : maxId), 0) +261;2728const newDrink: Drink = {29id: nextId,30title,31image,32volume,33price,34};3536drinks.push(newDrink);37return 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
:
jsapp.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 drinksGET 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:3000GET {{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:3000GET {{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 drinkGET {{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 drinkPOST {{baseUrl}}/drinksContent-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 surStart Debugging
(ou cliquez juste surF5
) en vérifiant que la configuration de debugging sélectionnée est bien nomméeLaunch 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éets-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 surF5
en vérifiant que la configuration de debugging sélectionnée est bien nomméets-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 :
URI | Méthode HTTP | Opération |
---|---|---|
films?minimum-duration=value | GET | READ ALL FILTERED : Lire toutes les ressources de la collection selon le filtre donné |
films/{id} | GET | READ ONE : Lire la ressource identifiée |
films | POST | CREATE 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 entiertitle
: 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 viareq.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 viareq.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 variableGET {{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 :
tsreturn 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 dataPOST {{baseUrl}}/drinksContent-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
:
ts1router.post("/", (req, res) => {2const body: unknown = req.body;3if (4!body ||5typeof body !== "object" ||6!("title" in body) ||7!("image" in body) ||8!("volume" in body) ||9!("price" in body) ||10typeof body.title !== "string" ||11typeof body.image !== "string" ||12typeof body.volume !== "number" ||13typeof body.price !== "number" ||14!body.title.trim() ||15!body.image.trim() ||16body.volume <= 0 ||17body.price <= 018) {19return res.sendStatus(400);20}2122const { title, image, volume, price } = body as NewDrink;2324const nextId =25drinks.reduce((maxId, drink) => (drink.id > maxId ? drink.id : maxId), 0) +261;2728const newDrink: Drink = {29id: nextId,30title,31image,32volume,33price,34};3536drinks.push(newDrink);37return 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 commeres.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 ledirector
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 :
tsrouter.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 elementsreturn 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 drinkDELETE {{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 :
tsrouter.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 5PATCH {{baseUrl}}/drinks/5Content-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 :
URI | Méthode HTTP | Opération |
---|---|---|
films/{id} | DELETE | DELETE ONE : Effacer la ressource identifiée |
films/{id} | PATCH | UPDATE 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} | PUT | UPDATE 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++
.