Ateliers Services Web

 

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.

Valentin MONTAGNE

1

Les architectures des services web

3

Le real-time data avec Websockets

5

QCM de fin de module et rendu des TPs et exercices

2

Concevoir et utiliser un web service en NodeJS

4

Bonus - Concevoir et utiliser un web service en Java et GraphQL

Déroulement du cours

Chaque séance débutera par la présentation d'un concept et de l'intérêt d'utilisation de celui-ci.

1

Théorie

Après la théorie, nous verrons alors la pratique en réalisant des exercices sur un repository gitlab.

2

Pratique

Nous verrons ensemble la correction des travaux pratiques. N'hésitez pas à poser vos questions.

3

Correction

Déroulement des journées

Connaissez-vous les services Web ?

Rendu TP et exercices

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

Les architectures des services web

1.

Comment internet fonctionne ?

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...

Comment fonctionne une requête ?

Comment fonctionne une requête ?

Qu'est-ce que le protocole

HTTP ?

En web, tout évolue année après année !

HTTP/1.1 - 1999

  • Methods : GET, POST, PUT, PATCH, OPTIONS, DELETE
  • Gère les transferts par morceau grâce au header "Content-length".
  • Caching
  • Client cookies
  • Tous les classiques d'aujourd'hui.

HTTP/2 - 2015

  • Passage en binaire - Streaming.
  • Server push - permet au serveur de renvoyer plusieurs réponses à une requête.
  • Amélioration de la sécurité

Quelles sont les méthodes

HTTP ?

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.

Quelles sont les statuts HTTP ?

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é.

En-têtes HTTP les plus connues

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.

La sécurité : HTTPS, TLS et SSL

Permet le transfère de donnée de manière sécurisée.

Lien vers une vidéo explicative.

Qu'est qu'un service DNS ?

Permet de référencer les adresses IPs sous une forme plus compréhensible par un humain.

Qu'est-ce qu'un service Web ?

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.

Les services Web XML

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.

Comment fonctionne WSDL ?

<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

Exemple WSDL pour SOAP

<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>

Comment fonctionne SOAP ?

Un message SOAP est un document XML ordinaire contenant les éléments suivants :

  • Un élément Enveloppe qui identifie le document XML comme un message SOAP.
  • Un élément Header qui contient des informations d'en-tête.
  • Un élément Body qui contient des informations sur l'appel et la réponse.
  • Un élément Fault contenant les erreurs et les informations d'état.

 

On peut y voir des ressemblances avec HTTP.

Exemple d'une requête HTTP avec SOAP

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>

TP - Réaliser un service Web SOAP en NodeJS

Un catalogue de jeux vidéo avec tous les détails

Un panier pour les jeux qui vont être achetés

MythicGames, la marketplace du jeu vidéo de demain.

Comment stocker les données de la Marketplace ?

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.

Débutons le service Web SOAP en NodeJS

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.

Rédigeons le fichier WSDL

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>

Rédigeons le fichier WSDL - 2

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>

Rédigeons le fichier WSDL - 3

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>

Rédigeons le fichier WSDL - 4

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>

Rédigeons le fichier WSDL - 5

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>

Rédigeons le fichier WSDL - 6

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>

Rédigeons le fichier WSDL - 7

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>

Réalisons le code côté serveur

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" });
      },
    },
  },
};

Réalisons le code côté serveur 2

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");
});

Réalisons le code côté client

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);
  });
});

C'est l'heure de notre première requête !

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

Réalisons la logique de CreateProduct

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" });
      },

Réalisons la logique de CreateProduct - 2

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')

Réalisons la logique de CreateProduct - 3

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

Réalisons la logique de CreateProduct - 4

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 *
`;

Réalisons la logique de CreateProduct - 5

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]);
      },

Réalisons la logique de CreateProduct - 6

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.

Exercice : Créer l'opération GetProducts

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>
       

Exercice : Créer l'opération PatchProduct et DeleteProduct

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.

Service Web REST

Qu'est-ce que REST ?

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.

Comment REST fonctionne ?

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.

Comment REST fonctionne ? - 2

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.

Comment REST fonctionne ? - 3

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.

Comment REST fonctionne ? - 4

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.

SOAP vs REST

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 aller plus loin sur REST.

TP - Réaliser un service Web REST en NodeJS

Comment tester notre service Web ?

Nous utiliserons la solution Insomnia dans l'esprit d'un Postman mais complètement Open Source et gratuit.

Débutons la réalisation de notre API REST en NodeJS

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.

Réalisons notre première ressource

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 notre première ressource - 2

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.

Réalisons notre première ressource - 3

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.

Réalisons notre première ressource - 4

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}`);
});

Réalisons notre première ressource - 5

Créons toutes les routes pour notre ressource "products" avec express, pour une ressource, voici les actions classiques à réaliser :

  • GET products/:id - Récupère un produit
  • GET products/ - Récupère tous les produits, en production, on utilise un système de pagination pour ne renvoyer par exemple que 10 par 10 les ressources.
  • POST products/ - Crée un nouveau produit grâce au body de la requête HTTP.
  • DELETE products/:id - Supprime un produit.

 

Par simplicité, nous ne ferons pas la méthode PUT et PATCH.

Réalisons notre première ressource - 6

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 ?

Réalisons notre première ressource - 7

// 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);
  }
});

Réalisons notre première ressource - 8

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 ?

Réalisons notre première ressource - 9

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.

Réalisons notre première ressource - 10

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" });
  }
});

Réalisons notre première ressource - 11

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" });
  }
});

Exercice 1 : Créer la ressource Users

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.

Et si on automatisait tout ça ?

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).

Exercice 2 - Intégrer un service Web

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).

Exercice 3 : Réaliser un vrai système de recherche

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 :

  • /products?name=game pour retrouver tous les jeux ayant dans leur titre "game".
  • /products?about=fps pour retrouver tous les jeux ayant dans leur "about" le mot "fps".
  • /products?price=30 pour retrouver tous les jeux ayant un prix inférieur ou égal à 30 euros.

N'hésitez pas à consulter la documentation de la librairie postgres pour réaliser les queries SQL :

https://www.npmjs.com/package/postgres#building-queries

Exercice 4 : Réaliser un système de panier

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 :

  • userId et productIds - L'id de l'user et des produits
  • total - Le total qui est le prix des produits * 1.2 (TVA)
  • payment - Boolean par défaut à false pour l'état du paiement
  • createdAt - La date de création de la commande
  • updatedAt - La date de la dernière modification de la commande

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.

Exercice 5 : Réaliser un système d'avis

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 :

  • userId et productId - L'id de l'user et du produit
  • score - Entre 1 à 5
  • content - Le message associé à l'avis
  • createdAt - La date de création de l'avis
  • updatedAt - La date de la dernière modification de l'avis

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.

Exercice 6 : Intégrer un système de documentation

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.

NoSQL, Websockets et GraphQL

2.

Pourquoi NoSQL ?

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.

Quand utiliser NoSQL ?

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.

MongoDB

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...).

Qu'est-ce que le Real-Time ?

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. 

Qu'est-ce que Websockets ?

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.

Qu'est-ce que Websockets ?

Exemple de requête

// 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 = "";
}

Les performances (Socket.io)

Les performances (Socket.io)

Les performances (Socket.io)

Qu'est-ce que le GraphQL ?

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é.

Pourquoi GraphQL ?

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.

Pourquoi GraphQL ?

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.

Exemple d'une requête

// 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
            }]
    }
}

TP - Réaliser une API REST en NodeJS en NoSQL

Mettons en place notre API NodeJS utilisant MongoDB

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

Intégrons le driver MongoDB avec Expressjs

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}`);
  });
});

Les opérations disponibles sur une MongoDB - 1

// 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 :

Les opérations disponibles sur une MongoDB - 2

// 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/

CreateProduct - 1

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.

CreateProduct - 2

// 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.

Les aggrégations - 1

// 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.

Les aggrégations - 2

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 });
  }
  ...

Les aggrégations - 3

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);
  }
});

Les aggrégations - 4

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.

Les aggrégations - 5

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.

Les Websockets

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.

Exercice : Finir Products

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.

Exercice : Créer une API REST d'analytics

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 :

  • /views - { source: string, url: string, visitor: string, createdAt: Date, meta: {} }
  • /actions - { source: string, url: string, action: string, visitor: string, createdAt: Date, meta: {} }
  • /goals - { source: string, url: string, goal: string, visitor: string, createdAt: Date, meta: {} }

Vérifiez vos routes en créant des ressources avec le champ "meta" différent, MongoDB n'a aucun problème avec ça.

Exercice : Agréger les analytics

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.

Bravo vous pouvez passer aux bonus !

En attendant le QCM.

Concevoir et utiliser un web service

Java

3.

Bonus

Découvrons JAX RS - Java API for RESTful Web Services

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 !

Concevoir et utiliser un web service

GraphQL

4.

Bonus

Découvrons GraphQL

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#

- Typescript

 

Le tutoriel permet de voir toutes les grandes notions de GraphQL.

Soutenance

QCM

30 minutes

5.

N'hésitez pas à me donner votre avis.

Merci!