Master Expert Technologie de l'information EPITECH 2020.
Co-fondateur et CTO d'une startup dans l'Edtech 2019 - fin 2022. (+3 ans)
Formation PSPO-1 Agile Scrum 2022.
Co-fondateur et CTO d'une startup dans la Deeptech fin 2022 - aujourd'hui.
Les architectures des services web
Le real-time data avec Websockets
QCM de fin de module et rendu des TPs et exercices
Concevoir et utiliser un web service en NodeJS
Bonus - Concevoir et utiliser un web service en Java et GraphQL
Chaque séance débutera par la présentation d'un concept et de l'intérêt d'utilisation de celui-ci.
Après la théorie, nous verrons alors la pratique en réalisant des exercices sur un repository gitlab.
Nous verrons ensemble la correction des travaux pratiques. N'hésitez pas à poser vos questions.
Pour faciliter le rendu final des TPs et exercices, vous allez créer un dépôt sur Github avec le nom WebServices en publique et le cloner sur votre machine.
Si vous préférez le garder en privé, vous devez m'ajouter avec les droits pour voir votre dépôt avec le nom d'utilisateur : ValentinMontagne
En 1960, les États-Unis cherche à créer un réseau décentralisé pour résister à une attaque nucléaire.
Un système complexe de router utilisant les protocoles IP et TCP pour envoyer les paquets.
Des protocoles apparaissent au fil des ans : UDP, DNS, HTTP, HTTPS, TLS, SSL, VOIP...
En web, tout évolue année après année !
HTTP/1.1 - 1999
HTTP/2 - 2015
GET - La requête récupère des données.
HEAD - Idem que GET, mais sans le corps de la réponse.
POST - La requête envoye de la donnée.
PUT - La requête remplace toute la donnée d'une ressource.
DELETE - La requête supprime une ressource.
PATCH - La requête remplace partiellement la donnée d'une ressource.
OPTIONS, CONNECT, TRACE - Les requêtes en lien avec la connexion avec le serveur.
200-226 - Représente les codes de succès.
300-308 - Représente les codes de succès avec redirection.
400-451 - Représente les codes d'erreurs côté client.
500-511 - Représente les codes d'erreurs côté serveur.
Voir liste complète des codes.
Il est important pour un service Web de répondre avec les bons codes pour permettre au client d'avoir le bon comportement.
Ex: 400 - Le formulaire envoyé n'a pas le bon format.
401 - Le client tente d'atteindre une ressource qui nécessite
d'être connecté.
Authorization - Contient les identifiants pour se connecter au serveur.
Accept-language - Contient les langages que le client supporte.
Cache-control - Gère la mise en cache côté client.
Content-type - Contient le type de la donnée envoyée.
Content-length - Contient la taille de la donnée envoyée.
Cookie & Set-Cookie - Contient les cookies déjà présents ou à créer côté client.
Voir liste complète des en-têtes.
Permet le transfère de donnée de manière sécurisée.
Lien vers une vidéo explicative.
Permet de référencer les adresses IPs sous une forme plus compréhensible par un humain.
Un service web est une technologie qui permet à différentes applications logicielles de communiquer et d'interagir entre elles via l'internet ou un réseau. Il permet l'interopérabilité entre différents systèmes, quels que soient les langages de programmation ou les plates-formes sur lesquels ils sont construits.
Les services web utilisent généralement des protocoles standard tels que HTTP, XML, SOAP et REST pour faciliter la communication entre les applications client et serveur.
XML-RPC est un protocole RPC (remote procedure call), qui permet à des processus s'exécutant dans des environnements différents de faire des appels de méthodes à travers un réseau. (Ancêtre de SOAP)
WSDL (Web Services Description Language) décrit un service web. Il spécifie l'emplacement du service et ses méthodes.
SOAP (Simple Object Access Protocol) permet de communiquer entre tout type d'applications en utilisant HTTP pour le transport et XML pour la structure de la donnée.
UDDI (Universal Description Discovery and Integration) permet la découverte de service Web grâce à WSDL et SOAP.
<types> - Définit les types de données (XML Schema) utilisés par le service web.
<message> - Définit les éléments de données pour chaque opération
<portType> - Décrit les opérations qui peuvent être effectuées et les messages impliqués.
<binding> - Définit le protocole et le format de données pour chaque type de port
<message name="getTermRequest">
<part name="term" type="xs:string"/>
</message>
<message name="getTermResponse">
<part name="value" type="xs:string"/>
</message>
<portType name="glossaryTerms">
<operation name="getTerm">
<input message="getTermRequest"/>
<output message="getTermResponse"/>
</operation>
</portType>
<binding type="glossaryTerms" name="b1">
<soap:binding style="document"
transport="http://schemas.xmlsoap.org/soap/http" />
<operation>
<soap:operation soapAction="http://example.com/getTerm"/>
<input><soap:body use="literal"/></input>
<output><soap:body use="literal"/></output>
</operation>
</binding>
Un message SOAP est un document XML ordinaire contenant les éléments suivants :
On peut y voir des ressemblances avec HTTP.
POST /InStock HTTP/1.1
Host: www.example.org
Content-Type: application/soap+xml; charset=utf-8
Content-Length: 299
SOAPAction: "http://www.w3.org/2003/05/soap-envelope"
<?xml version="1.0"?>
<soap:Envelope
xmlns:soap="http://www.w3.org/2003/05/soap-envelope"
xmlns:m="http://www.example.org"
>
<soap:Header>
</soap:Header>
<soap:Body>
<m:GetStockPrice>
<m:StockName>T</m:StockName>
</m:GetStockPrice>
</soap:Body>
</soap:Envelope>
Un catalogue de jeux vidéo avec tous les détails
Un panier pour les jeux qui vont être achetés
Nous utiliserons la base de donnée PostgresSQL durant ce TP.
Je vous conseille d'utiliser une image Docker directement, sinon, voici un lien vers la documentation d'installation.
Pourquoi choisir SQL ?
Une base de donnée relationnelle est le choix parfait quand vous devez stocker un grand nombre d'éléments qui ont des relations entre eux.
Par exemple : un produit de la marketplace pourrait posséder des catégories qui seront définies dans une autre table, on peut alors assignés les IDs des catégories dans un produit.
Pour commencer, nous allons avoir besoin d'installer NodeJS.
Une fois fait, créer un dossier SOAP où nous allons créer tous les fichiers en rapport avec le service Web.
Pour initier le projet, lancer la commande :
npm init
Nous allons utiliser la librairie soap, lancer la commande :
npm install soap
Attention, SOAP n'étant plus aussi utilisé qu'avant, nous ne réaliserons ici que la surface du service Web pour nous concentrer sur REST plus tard.
Nous allons commencer par créer le service ProductsService avec l'opération CreateProduct pour ajouter un produit en base de donnée.
Créons le fichier "productsService.wsdl".
Pour définir le service nous allons utiliser l'élément "definitions" avec pour attributs : "name", "targetNamespace" qui est l'URL où le service sera appelé et les attributs XML obligatoires pour définir que le service utilisera SOAP et XMLSchema.
<definitions name="ProductsService"
targetNamespace="http://localhost:8000/products"
xmlns:tns="http://localhost:8000/products"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
</definitions>
Créons ensuite les types de la requête et la réponse à notre opération "CreateProduct", pour cela nous allons utiliser les types de XML Schema.
Nous allons créer un type "CreateProductRequest" représentant la requête du client et possédant 3 éléments : "name", "about", "price".
<definitions>
<types>
<xsd:schema targetNamespace="http://localhost:8000/products">
<xsd:element name="CreateProductRequest">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="name" type="xsd:string"/>
<xsd:element name="about" type="xsd:string"/>
<xsd:element name="price" type="xsd:decimal"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</types>
</definitions>
Créons le type "CreateProductResponse" représentant la reponse du serveur et possédant les 3 éléments précédents plus l'élément "id" du nouveau produit créé.
<definitions>
<types>
<xsd:schema targetNamespace="http://localhost:8000/products">
<xsd:element name="CreateProductRequest">
...
</xsd:element>
<xsd:element name="CreateProductResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="id" type="xsd:string"/>
<xsd:element name="name" type="xsd:string"/>
<xsd:element name="about" type="xsd:string"/>
<xsd:element name="price" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:schema>
</types>
</definitions>
Créons maintenant les messages qui seront utilisés pour SOAP en utilisant les types que nous avons créés précédemment.
<definitions>
<types>
...
</types>
<message name="CreateProductRequestMessage">
<part name="request" element="tns:CreateProductRequest"/>
</message>
<message name="CreateProductResponseMessage">
<part name="response" element="tns:CreateProductResponse"/>
</message>
</definitions>
Créons le port qui sera utilisé pour SOAP en utilisant les messages que nous avons créés précédemment. Nous déclarons ici l'opération "CreateProduct" avec en input la request et en output la response.
<definitions>
<types>
...
</types>
<message name="CreateProductRequestMessage">
<part name="request" element="tns:CreateProductRequest"/>
</message>
<message name="CreateProductResponseMessage">
<part name="response" element="tns:CreateProductResponse"/>
</message>
<portType name="ProductsPortType">
<operation name="CreateProduct">
<input message="tns:CreateProductRequestMessage"/>
<output message="tns:CreateProductResponseMessage"/>
</operation>
</portType>
</definitions>
Créons le binding à SOAP en utilisant le port nous avons créés précédemment. Nous déclarons ici "ProductsBinding" qui utilisera HTTP pour déclarer l'opération CreateProduct sur l'URL "http://example.com/products/CreateProduct"
<definitions>
<types>...</types>
<message>...</message>
<portType>...</portType>
<binding name="ProductsBinding" type="tns:ProductsPortType">
<soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
<operation name="CreateProduct">
<soap:operation soapAction="http://localhost:8000/products/CreateProduct"/>
<input>
<soap:body use="encoded" namespace="http://localhost:8000/products"
encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
</input>
<output>
<soap:body use="encoded" namespace="http://localhost:8000/products"
encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
</output>
</operation>
</binding>
</definitions>
Finalement, créons le service SOAP en utilisant le binding que nous avons créés précédemment. Nous déclarons ici "ProductsService" sur l'URL "http://example.com/products"
<definitions>
<types>...</types>
<message>...</message>
<portType>...</portType>
<binding>...</binding>
<service name="ProductsService">
<port name="ProductsPort" binding="tns:ProductsBinding">
<soap:address location="http://localhost:8000/products"/>
</port>
</service>
</definitions>
Créons un fichier "server.js" à la racine du dossier.
Importons "soap", "node:fs" et "http" et définissons le service SOAP que nous avons réalisé dans le fichier précédent avec une function pour son opération CreateProduct.
const soap = require("soap");
const fs = require("node:fs");
const http = require("http");
// Define the service implementation
const service = {
ProductsService: {
ProductsPort: {
CreateProduct: function (args, callback) {
// Log args received
console.log("ARGS : ", args);
// Send response with args and fake id.
callback({ ...args, id: "myid" });
},
},
},
};
Ajoutons un serveur HTTP en nodejs écoutant sur le port 8000 puis créons le serveur SOAP en lui envoyant en paramètre le serveur HTTP, l'URL qu'il doit utiliser, le service définit et l'XML du fichier "productsService.wsdl".
// http server example
const server = http.createServer(function (request, response) {
response.end("404: Not Found: " + request.url);
});
server.listen(8000);
// Create the SOAP server
const xml = fs.readFileSync("productsService.wsdl", "utf8");
soap.listen(server, "/products", service, xml, function () {
console.log("SOAP server running at http://localhost:8000/products?wsdl");
});
Créons le fichier "client.js" et importons "soap" à nouveau. Créons le client SOAP grâce au fichier WSDL fournit par notre serveur. Ensuite envoyons en args à l'opération CreateProduct : { name: "My product" }.
const soap = require("soap");
soap.createClient("http://localhost:8000/products?wsdl", {}, function (err, client) {
if (err) {
console.error("Error creating SOAP client:", err);
return;
}
// Make a SOAP request
client.CreateProduct({ name: "My product" }, function (err, result) {
if (err) {
console.error(
"Error making SOAP request:",
err.response.status,
err.response.statusText,
err.body
);
return;
}
console.log("Result:", result);
});
});
Lançons sur un terminal le serveur et sur un autre le client.
Que constatez-vous ?
Étrangement, la requête fonctionne même si l'on ne n'envoie pas les bons arguments. C'est pour cela que nous avons des outils pour déclarer et valider des schémas, ici nous irons à la simplicité et nous allons vérifier que nous avons les bons arguments avec un if.
// Term 1
node server.js
// Term 2
node client.js
Nous avons ajouté un if pour throw une erreur, en SOAP, c'est une Fault. Nous changeons le code de la réponse HTTP en ajoutant "statusCode: X".
Quel statut devriez-vous mettre dans cette situation ?
CreateProduct: function ({ name, about, price }, callback) {
if (!name || !about || !price) {
throw {
Fault: {
Code: {
Value: "soap:Sender",
Subcode: { value: "rpc:BadArguments" },
},
Reason: { Text: "Processing Error" },
statusCode: X,
},
};
}
callback({ ...args, id: "myid" });
},
Ajoutons la création en base de donnée de notre produit. Installons la dépendance postgres pour nodeJS.
npm i postgres
Créons un fichier "init.sql" pour initialiser PostgresSQL à son lancement avec une table "products" suivant notre schéma.
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
about VARCHAR(500),
price FLOAT
);
INSERT INTO products (name, about, price) VALUES
('My first game', 'This is an awesome game', '60')
Lançons notre base de donnée PostgresSQL avec docker.
docker run --name postgres -p 5432:5432 \
-e POSTGRES_USER=user -e POSTGRES_PASSWORD=password -e POSTGRES_DB=mydb \
-v ./init.sql:/docker-entrypoint-initdb.d/init.sql -d postgres
Ajoutons dans server.js la logique pour se connecter à la base de donnée :
const soap = require("soap");
const fs = require("node:fs");
const http = require("http");
const postgres = require("postgres");
const sql = postgres({ db: "mydb", user: "user", password: "password" });
...
# Attention pour Windows, vous devez utiliser un chemin absolu
${PWD}/init.sql:/docker-entrypoint-initdb.d/init.sql
Ajoutons la logique pour créer un nouveau jeu dans notre table "products" à chaque opération "CreateProduct". Attention, on ajoute le mot clé "async" devant la fonction pour indiquer qu'on peut utiliser "await" pour attendre la réponse de notre base de donnée. (Doc)
Nous réaliserons la requête SQL suivante pour insérer dans la base de donnée :
const product = await sql`
INSERT INTO products (name, about, price)
VALUES (${name}, ${about}, ${price})
RETURNING *
`;
Ce qui donne la logique suivante :
CreateProduct: async function ({ name, about, price }, callback) {
if (!name || !about || !price) {
throw {
Fault: {
Code: {
Value: "soap:Sender",
Subcode: { value: "rpc:BadArguments" },
},
Reason: { Text: "Processing Error" },
statusCode: 400,
},
};
}
const product = await sql`
INSERT INTO products (name, about, price)
VALUES (${name}, ${about}, ${price})
RETURNING *
`;
// Will return only one element.
callback(product[0]);
},
Vérifions que tout est fonctionnel, réalisons une requête invalide en n'envoyant que le nom et le prix côté client pour vérifier que l'on reçoit une erreur. Ensuite, envoyez avec un prix pour vérifier que vous recevez un produit avec un nouvel ID.
Appelez-moi pour que l'on puisse vérifier ensemble que tout est bon, faites de même pour chaque fin d'exercice.
Maintenant que vous êtes un expert du SOAP, essayez d'ajouter une nouvelle opération à ce service du nom de "GetProducts". Son but est de renvoyer au client tous les produits présent dans la base de donnée. Petite aide sur la définition du schéma, vous pouvez définir un tableau d'objet comme ceci :
<xsd:element name="GetProductsResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element maxOccurs="unbounded" ref="Product" />
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="Product">
<xsd:element name="id" type="xsd:string" />
<xsd:element name="name" type="xsd:string" />
<xsd:element name="about" type="xsd:string" />
<xsd:element name="price" type="xsd:string" />
</xsd:element>
</xsd:schema>
Ajouter deux nouvelles opérations à ce service du nom de "PatchProduct" et "DeleteProduct".
Le but de la première est de mettre à jour une partie d'un produit en base de donnée grâce à son ID.
La seconde est de supprimer un produit en base de donnée grâce à son ID.
Attention à vérifier que l'on vous envoie bien tous les arguments et gérer correctement les cas d'erreurs.
REST, ou REpresentational State Transfer, est un style architectural qui fournit des normes entre les systèmes informatiques sur le web, facilitant ainsi la communication entre les systèmes.
Ces systèmes se caractérisent par le fait qu'ils sont sans état et qu'ils séparent le client et le serveur.
REST utilise HTTP pour réaliser des opérations sur des Ressources. Les ressources peuvent être n'importe quelle donnée qui doit être traité ou stocké. Comme ici nos produits pour notre Marketplace.
REST se base totalement sur HTTP pour répondre aux requêtes des clients pour récupérer ou modifier des ressources. En suivant les méthodes HTTP et une certaine logique sur l'URL, le service web applique alors un comportement.
On parle souvent de CRUD pour désigner les actions possibles à appliquer à une ressource, par convention une ressource est au pluriel dans l'URL de l'API.
Exemple :
GET /articles/23
Permet de récupérer l'article avec l'id 23.
POST /articles
{ title: "My article", author: "ID" }
Permet de créer un nouvel article.
Quel est le format du POST ? Adieu XML, bonjour JSON !
Le JSON est un format moins complexe et plus facile à lire par un humain. Ce format est très similaire aux objets en Javascript.
C'est maintenant le format par défaut pour les services REST.
Il est possible de réaliser simplement depuis une URL des comportements complexes. Par exemple, une URL permettant de récupérer tous les posts d'un utilisateur et dans le titre "post" :
GET /users/1/posts?title=post
[{ id: '1', title: 'My post'}, { id: '2', title: 'Other post' }]
GET /users/1/posts/1
{ id: '1', title: 'My post' }
GET /users/1/posts/3
404 Not Found
Cela permet un comportement bien plus intuitif et permet au client de réagir directement au réponse HTTP du serveur.
En REST, toutes les routes permettent de gérer des ressources, une règle importante et que chaque action sur une ressource doit renvoyer la ressource qui a reçu l'action.
Par exemple :
PATCH /users/1
{ email: "valentinm@example.fr" }
Renvoie :
{ id: "X", "email": "valentinm@example.fr", name: "Valentin", ... }
Cela permet un comportement bien plus intuitif et permet au client de récupérer les retours des appels à l'API pour mettre à jour l'interface.
En raison du nombre d'informations qu'impose le format XML, SOAP peut alourdir considérablement les échanges ce qui est un handicap quand les volumes de données transités par SOAP sont faibles par rapport au volume total de données échangées.
SOAP décrit la manière dont les applications doivent communiquer entre elles, certains considèrent que le couplage reste fort entre le serveur et ses clients. Une modification de l'API implique ainsi une évolution côté client, contrairement à une architecture orientée ressources telle que REST.
Adieu WSDL, les comportements de l'API REST sont intuitifs, une simple documentation permet de gérer les cas particuliers.
Pour commencer, créons un dossier REST-POSTGRES où nous allons créer tous les fichiers en rapport avec le service Web.
Pour initier le projet, lancer la commande :
npm init
Nous allons utiliser la librairie express, lancer la commande :
npm install express
Créons un fichier "server.js" qui contiendra toute la logique de notre service REST.
Comme pour notre service SOAP, nous utiliserons la base de donnée PostgresSQL et le même init.sql, nous allons donc relancer celle-ci via Docker pour éviter d'avoir les données de l'ancien service.
# Remove postgres container.
docker container rm postgres -f
docker run --name postgres -p 5432:5432 \
-e POSTGRES_USER=user -e POSTGRES_PASSWORD=password -e POSTGRES_DB=mydb \
-v ./init.sql:/docker-entrypoint-initdb.d/init.sql -d postgres
Nous allons faire la ressource "/products" qui représentera tous les jeux vidéos de la Marketplace. REST n'a pas de système de schéma comme SOAP par défaut, on utilise alors une librairie, ici nous allons utiliser la librairie Zod.
npm install postgres zod
Réalisons le Hello World de Express et vérifions d'avoir Hello World en ouvrant le navigateur. Ajoutons aussi le parser JSON pour avoir accès au JSON via request.body.
const express = require("express");
const app = express();
const port = 8000;
app.use(express.json());
app.get("/", (req, res) => {
res.send("Hello World!");
});
app.listen(port, () => {
console.log(`Listening on http://localhost:${port}`);
});
Ajoutons à nouveau la logique de la base de donnée en utilisant la librairie "postgres" comme dans notre précédent service.
Ajoutons la librairie "zod" pour réaliser notre premier schéma "ProductSchema". On crée un schéma via la méthode "object", chaque propriété de l'objet passé en argument peut contenir un type de Zod.
Par exemple, pour price, on utilise z.number() pour que Zod puisse vérifier que price est bien un nombre. Les types peuvent avoir des comportements particuliers comme ici avec la méthode "positive" nous demandons à Zod de vérifier si price est bien un nombre positif. (Évitons de permettre des prix négatifs sur notre Marketplace !)
Voir le code à la prochaine slide.
Cela donne ce résultat :
const express = require("express");
const postgres = require("postgres");
const z = require("zod");
const app = express();
const port = 8000;
const sql = postgres({ db: "mydb", user: "user", password: "password" });
app.use(express.json());
// Schemas
const ProductSchema = z.object({
id: z.string(),
name: z.string(),
about: z.string(),
price: z.number().positive(),
});
app.get("/", (req, res) => {
res.send("Hello World!");
});
app.listen(port, () => {
console.log(`Listening on http://localhost:${port}`);
});
Créons toutes les routes pour notre ressource "products" avec express, pour une ressource, voici les actions classiques à réaliser :
Par simplicité, nous ne ferons pas la méthode PUT et PATCH.
Ajoutons la logique de la route POST pour créer un produit en vérifiant le body de la requête grâce à Zod pour valider notre schéma avant de l'envoyer à la base de donnée.
Nous allons pour ça créer un schéma spécial pour la requête POST en utilisant la méthode "z.omit()" pour retirer la propriété "id" qui n'existe pas pour un nouveau produit.
Attention à bien utiliser l'objet "response" avec la méthode "status" de express pour gérer les cas d'erreur avec les bons code de statut.
Ici, si le schéma ne valide pas le body de la requête, quelle réponse doit-on renvoyer ?
// Schemas
const ProductSchema = z.object({
id: z.string(),
name: z.string(),
about: z.string(),
price: z.number().positive(),
});
const CreateProductSchema = ProductSchema.omit({ id: true });
app.post("/products", async (req, res) => {
const result = await CreateProductSchema.safeParse(req.body);
// If Zod parsed successfully the request body
if (result.success) {
const { name, about, price } = result.data;
const product = await sql`
INSERT INTO products (name, about, price)
VALUES (${name}, ${about}, ${price})
RETURNING *
`;
res.send(product[0]);
} else {
res.status(400).send(result);
}
});
Sur Insomnia, créons une collection "Products" pour mettre toutes nos futures requêtes. Créons une requête POST sur la route /products avec pour body du JSON.
N'envoyez pas la propriété "about", que se passe-t-il ?
Créons les routes pour récupérer tous les produits ou un seul.
Attention, pour récupérer un produit on va donc créer la route /products/:id et utiliser request.params.id pour accéder à l'ID.
Un cas d'erreur est possible ici, qu'est-ce que c'est ? Quel est le statut de la requête ?
N'oubliez pas de tester avec Insomnia pour chaque route.
Il faut vérifier si l'ID est bien dans la base de donnée. Sinon nous devons renvoyer une erreur 404.
app.get("/products", async (req, res) => {
const products = await sql`
SELECT * FROM products
`;
res.send(products);
});
app.get("/products/:id", async (req, res) => {
const product = await sql`
SELECT * FROM products WHERE id=${req.params.id}
`;
if (product.length > 0) {
res.send(product[0]);
} else {
res.status(404).send({ message: "Not found" });
}
});
Ajoutons la route DELETE pour supprimer un produit.
Avec REST, on part du principe que pour chaque action, on renvoie la version mise à jour de la ressource. C'est pour cela qu'ici nous retournons la ressource qui vient d'être supprimée.
app.delete("/products/:id", async (req, res) => {
const product = await sql`
DELETE FROM products
WHERE id=${req.params.id}
RETURNING *
`;
if (product.length > 0) {
res.send(product[0]);
} else {
res.status(404).send({ message: "Not found" });
}
});
Maintenant que vous êtes un expert du REST, essayez d'ajouter une nouvelle ressource Users. La ressource doit avoir un nom d'utilisateur, un mot de passe et un email.
Attention, on ne doit jamais récupérer le mot de passe de l'utilisateur dans les réponses du serveur et le mot de passe ne doit pas être en clair en base de donnée (SHA512).
Vous devez ajouter les actions PUT et PATCH pour cette ressource, attention à bien gérer les cas d'erreurs.
(PUT - Update toute la ressource, PATCH - Update partiellement ; par exemple seulement l'email).
Appelez-moi pour que l'on puisse vérifier ensemble que tout est bon.
Une impression de déjà-vu non ? Oui, REST ayant un fonctionnement suivant un système bien huilé, des développeurs ont vite vu qu'ils pouvaient automatiser la création de route REST.
Plusieurs projets existent, un très connu pour sa rapidité et sa facilité d'utilisation est FeathersJS.
Ce n'est pas demandé pour notre TP, mais en production évitons de réinventer la roue !
(Fun fact : c'est avec cette technologie que j'ai réalisé l'API de ma première Startup avec une base de donnée MongoDB).
Maintenant que nous avons réaliser plusieurs service Web en SOAP ou REST, nous allons intégrer dans notre API REST un autre service web.
Notre Marketplace aimerait mettre en avant une sélection de jeux Free-to-Play, pour cela vous avez identifié un service FreeToGame.
Créez une nouvelle ressources /f2p-games et utilisez fetch pour récupérer les résultats sur le service web au lieu de la base de donnée. (Seulement les requêtes GET / et GET /:id sont à faire ici).
Pour aller plus loin, réalisez un système de recherche sur la ressource /products. Ajouter des paramètres à envoyer pour mieux filtrer la liste des produits par exemple :
N'hésitez pas à consulter la documentation de la librairie postgres pour réaliser les queries SQL :
Pour aller plus loin, réalisez un système de panier avec une nouvelle ressource "Orders".
Pour cela la ressource doit posséder les éléments suivants :
La logique de paiement n'est pas attendu, toutes les méthodes REST sont à faire et il faut renvoyer l'user et les produits complet avec la méthode GET.
Maitenant que l'on a des achats, réalisez un système d'avis avec une nouvelle ressource "Reviews". Pour cela la ressource doit posséder les éléments suivants :
Il faut aussi mettre à jour la ressource "products" pour lui ajouter les ids des reviews et le score total à chaque création d'une review et permettre que lorsque l'on réalise un GET d'un produit, que l'on reçoit dans la donnée du produit, les reviews aussi.
Pour aller plus loin, intégrer la solution Swagger à votre API pour documenter votre service web REST.
La documentation est primordiale pour une API REST pour permettre aux clients de vraiment connaître les cas d'erreurs, les comportements et résultats de vos ressources.
Une flexibilité à toute épreuve par son système de collections et d'index.
La donnée est sous forme de document.
Haute-disponibilité sur énormément de données.
Une donnée n'ayant pas un schéma particulier.
L'écriture régulière de donnée dans des documents.
Développement rapide en environnement Agile / startup.
Sinon, pas d'hésitations, SQL sera la bonne réponse à 90%
de vos cas d'usages.
Base de donnée créée en 2009 en C++ pour du Cloud Computing.
Créée pour le Cloud et être utilisée en Cluster.
Des fonctionnalités de bases très puissantes (Geosearch, Fuzzysearch, Aggregations...).
Le serveur et le client communiquent en direct.
Permettre au client de réagir à des évènements côté serveur sans faire de nouvelles requêtes.
Des performances supérieure pour une impression d'instantanée.
Protocole réseau créé en 2011 par l'IETF (Internet Engineering Task Force) et W3School.
Utilise aussi TCP et les ports 80 et 443. (WS et WSS)
N'est disponible que sur les navigateurs web.
// Send text to all users through the server
function sendText() {
// Construct a msg object containing the data the server needs to process the message
// from the chat client.
const msg = {
type: "message",
text: document.getElementById("text").value,
id: clientID,
date: Date.now(),
};
// Send the msg object as a JSON-formatted string.
exampleSocket.send(JSON.stringify(msg));
// Blank the text input element, ready to receive the next line of text from the user.
document.getElementById("text").value = "";
}
Langage de requête créé en 2012 par Facebook.
(Open source depuis 2015)
Alternative au REST, le client décide de la structure de donnée qu'il veut recevoir du serveur.
Évite le over ou under-fetching en REST et est fortement typé.
Permet au client de structurer les données en fonction de l'utilisateur.
Optimise les données échangées.
(Cache côté client)
Optimise les performances réseaux.
Un grand pouvoir implique de grandes responsabilités !
GraphQL permet au client d'être plus indépendant, mais cela peut-être à double tranchant. Le client peut ne pas connaître ce qui est le moins performant côté serveur et peut créer des requêtes lourdes et peu performantes facilement.
Il est aussi impossible de créer un cache côté serveur vu que les requêtes sont changeantes d'un client à l'autre.
Sinon, pas d'hésitations, REST sera la bonne réponse à 90%
de vos cas d'usages.
// Request
{
orders {
id
productsList {
product {
name
price
}
quantity
}
totalAmount
}
}
// Response
{
"data": {
"orders": [{
"id": 0,
"productsList": [{
"product": {
"name": "orange",
"price": 1.5
},
"quantity": 100
}],
"totalAmount": 150
}]
}
}
Pour commencer, nous allons reprendre les consignes du TP pour créer une API REST en utilisant Postgres ici, créons un nouveau dossier "REST-MONGODB".
Le but sera de créer à nouveau la ressource Products comme dans le précédent TP mais en utilisant MongoDB.
Voici la commande pour créer une base de donnée MongoDB avec Docker :
Il suffit ensuite de d'initialiser le projet et d'installer le driver mongoDB avec npm :
npm init
npm install mongodb
docker run --name mongodb -p 27017:27017 -d mongo:latest
Importons mongodb et connectons le client à la base de donnée avant que le serveur ne reçoive des requêtes.
// All other imports here.
const { MongoClient } = require("mongodb");
const app = express();
const port = 8000;
const client = new MongoClient("mongodb://localhost:27017");
let db;
app.use(express.json());
// Product Schema + Product Route here.
// Init mongodb client connection
client.connect().then(() => {
// Select db to use in mongodb
db = client.db("myDB");
app.listen(port, () => {
console.log(`Listening on http://localhost:${port}`);
});
});
// Insert a Document
// Add to app.js the following function which uses the insertMany method to add three documents to the documents collection.
const insertResult = await collection.insertMany([{ a: 1 }, { a: 2 }, { a: 3 }]);
console.log('Inserted documents =>', insertResult);
// The insertMany command returns an object with information about the insert operations.
// Find All Documents
// Add a query that returns all the documents.
const findResult = await collection.find({}).toArray();
console.log('Found documents =>', findResult);
// This query returns all the documents in the documents collection. If you add this below the insertMany example you'll see the document's you've inserted.
// Find Documents with a Query Filter
// Add a query filter to find only documents which meet the query criteria.
const filteredDocs = await collection.find({ a: 3 }).toArray();
console.log('Found documents filtered by { a: 3 } =>', filteredDocs);
// Only the documents which match 'a' : 3 should be returned.
Avant de développer nos routes, nous allons voir des exemples d'opérations possibles avec MongoDB :
// Update a document
// The following operation updates a document in the documents collection.
const updateResult = await collection.updateOne({ a: 3 }, { $set: { b: 1 } });
console.log('Updated documents =>', updateResult);
// The method updates the first document where the field a is equal to 3 by adding a new field b to the document set to 1. updateResult contains information about whether there was a matching document to update or not.
// Remove a document
// Remove the document where the field a is equal to 3.
const deleteResult = await collection.deleteMany({ a: 3 });
console.log('Deleted documents =>', deleteResult);
// Index a Collection
// Indexes can improve your application's performance. The following function creates an index on the a field in the documents collection.
const indexName = await collection.createIndex({ a: 1 });
console.log('index name =', indexName);
// Full documentation : https://www.mongodb.com/docs/drivers/node/current/
app.post("/products", async (req, res) => {
const result = await CreateProductSchema.safeParse(req.body);
// If Zod parsed successfully the request body
if (result.success) {
const { name, about, price } = result.data;
const ack = await db
.collection("products")
.insertOne({ name, about, price });
res.send({ _id: ack.insertedId, name, about, price });
} else {
res.status(400).send(result);
}
});
Ajoutons le post sur la route /products pour créer un product.
Avec MongoDB, .insertOne renvoie un objet qui contient l'id du nouvel élément. Par convention en REST, la méthode POST doit renvoyer l'élément, c'est pour cela que l'on renvoie la donnée.
On peut aussi GET directement le nouvel élément depuis la base de donnée si des modifications ont lieu, un cas plus rare.
// Schemas
const ProductSchema = z.object({
_id: z.string(),
name: z.string(),
about: z.string(),
price: z.number().positive(),
});
const CreateProductSchema = ProductSchema.omit({ _id: true });
Attention, comme vous pouvez le voir dans la création de product, nous ajoutons le champ "_id".
Par défaut en MongoDB le champ d'id s'appelle "_id", nous allons donc devoir modifier notre Schéma.
// Schemas
const ProductSchema = z.object({
_id: z.string(),
name: z.string(),
about: z.string(),
price: z.number().positive(),
categoryIds: z.array(z.string())
});
const CreateProductSchema = ProductSchema.omit({ _id: true });
const CategorySchema = z.object({
_id: z.string(),
name: z.string(),
});
const CreateCategorySchema = CategorySchema.omit({ _id: true });
MongoDB n'était pas une base de donnée SQL, il est impossible de joindre facilement plusieurs documents ensemble dans la même requête que pour récupérer les documents d'une collection.
Pour cela, nous allons utiliser une agrégation, qui est un outil capable de faire beaucoup plus qu'une jointure. Nous allons créer une ressource Categories et nous allons ajouter au schéma de la ressource Products un tableau d'id de Category.
app.post("/categories", async (req, res) => {
const result = await CreateCategorySchema.safeParse(req.body);
// If Zod parsed successfully the request body
if (result.success) {
const { name } = result.data;
const ack = await db.collection("categories").insertOne({ name });
res.send({ _id: ack.insertedId, name });
} else {
res.status(400).send(result);
}
});
Maintenant, créons la route permettant de créer une Category et ajoutons le nouveau tableau dans le CREATE de products.
app.post("/products", async (req, res) => {
...
const { name, about, price, categoryIds } = result.data;
const ack = await db
.collection("products")
.insertOne({ name, about, price, categoryIds });
res.send({ _id: ack.insertedId, name, about, price, categoryIds });
}
...
Attention, avec MongoDB, les Ids ont besoin d'être du type "ObjectId" et possède un certains format. Il est impossible de faire des agrégations entre un véritable id et un id sous forme de string.
app.post("/products", async (req, res) => {
const result = await CreateProductSchema.safeParse(req.body);
// If Zod parsed successfully the request body
if (result.success) {
const { name, about, price, categoryIds } = result.data;
const categoryObjectIds = categoryIds.map((id) => new ObjectId(id));
const ack = await db
.collection("products")
.insertOne({ name, about, price, categoryIds: categoryObjectIds });
res.send({
_id: ack.insertedId,
name,
about,
price,
categoryIds: categoryObjectIds,
});
} else {
res.status(400).send(result);
}
});
app.get("/products", async (req, res) => {
const result = await db
.collection("products")
.aggregate([
{ $match: {} },
{
$lookup: {
from: "categories",
localField: "categoryIds",
foreignField: "_id",
as: "categories",
},
},
])
.toArray();
res.send(result);
});
Ajoutons le GET de products pour notre agrégation.
Nous allons ajouter un premier élément dans la pipeline qui est "$match", c'est ici que l'on peut écrire la query pour filtrer les éléments.
On ajoute ensuite "$lookup" qui permet de faire un Join.
Il n'y a plus qu'à vérifier que tout fonctionne.
Créez une catégorie, ajouter son id dans un nouveau produit et faites une requête pour récupérer la liste des produits, normalement vous devriez avoir un champ "categories" dans vos résultats.
Appelez-moi quand vous avez terminé pour valider ensemble.
Pour cette partie, nous allons réaliser le tutoriel de la librairie Socket.io jusqu'à l'étape 5 dans un nouveau dossier "WEBSOCKETS".
Ce tutoriel explique toutes les bases d'une gestion de websockets avec l'exemple d'un chat :
https://socket.io/fr/docs/v4/tutorial/step-1
Appelez-moi quand vous avez terminé pour valider ensemble.
Nous avons réaliser le CREATE pour la ressource Products, nous devons maintenant réaliser toutes les autres routes pour respecter REST et le CRUD.
Attention à bien respecter les cas d'erreurs avec les bons code de statut sur vos réponses.
Appelez-moi dès que vous avez terminé pour valider ensemble.
MongoDB est une très bonne solution pour stocker régulièrement des logs ou des documents qui sont très flexibles en fonction des meta-données envoyées. Réaliser une API REST dans un nouveau dossier REST-ANALYTICS avec les ressources suivantes :
Vérifiez vos routes en créant des ressources avec le champ "meta" différent, MongoDB n'a aucun problème avec ça.
Ajouter dans la route goals la route "/goals/:goalId/details" pour récupérer un Goal et via une agrégation tous les views et actions associés à ce visiteur.
Appelez-moi quand vous avez terminé pour valider ensemble.
En attendant le QCM.
Bonus
Pour ce TP, je vous propose de réaliser les exercices 1 à 3 créé par Mickael Baron qui est enseignant à ISAE-ENSMA et à l'Université de Poitiers. Le TP est très qualitatif et je trouvais inutile de refaire ce travail.
Voici le lien du TP :
https://github.com/mickaelbaron/jaxrs-tutorial/tree/master/jaxrs-tutorial-exercice1
Merci à Mickael de mettre ces ressources en ligne !
Bonus
Pour ce TP, je vous propose de réaliser le tutoriel du site internet www.apollographql.com pour s'initier à GraphQL.
Le tutoriel est disponible en :
- Java
- C#
Le tutoriel permet de voir toutes les grandes notions de GraphQL.