Atelier sur les API Web sans serveur

1. Présentation

L'objectif de cet atelier de programmation est d'acquérir de l'expérience avec proposés par Google Cloud Platform:

  • Cloud Functions : permet de déployer de petites unités de logique métier sous la forme de fonctions qui réagissent à divers événements (messages Pub/Sub, nouveaux fichiers dans Cloud Storage, requêtes HTTP, etc.).
  • App Engine : pour déployer et diffuser des applications Web, des API Web, des backends mobiles et des éléments statiques, avec des fonctionnalités de scaling à la hausse ou à la baisse rapides
  • Cloud Run, qui permet de déployer et de faire évoluer des conteneurs pouvant contenir n'importe quel langage, environnement d'exécution ou bibliothèque.

Vous découvrirez comment exploiter ces services sans serveur pour déployer et faire évoluer des API Web et REST, tout en vous familiarisant avec les principes de conception RESTful.

Dans cet atelier, nous allons créer un explorateur d'étagères composé des éléments suivants:

  • Une fonction Cloud: pour importer l'ensemble de données initial de livres disponibles dans notre bibliothèque, dans la base de données de documents Cloud Firestore,
  • Un conteneur Cloud Run: qui exposera une API REST sur le contenu de la base de données
  • Une interface Web App Engine: pour parcourir la liste des livres, en appelant notre API REST

Voici à quoi ressemblera l'interface Web à la fin de cet atelier de programmation:

705e014da0ca5e90.png

Points abordés

  • Cloud Functions
  • Cloud Firestore
  • Cloud Run
  • App Engine

2. Préparation

Configuration de l'environnement au rythme de chacun

  1. Connectez-vous à la console Google Cloud, puis créez un projet ou réutilisez un projet existant. (Si vous ne possédez pas encore de compte Gmail ou Google Workspace, vous devez en créer un.)

295004821bab6a87.png

37d264871000675d.png

96d86d3d5655cdbe.png

  • Le nom du projet est le nom à afficher pour les participants au projet. Il s'agit d'une chaîne de caractères non utilisée par les API Google. Vous pourrez toujours le modifier.
  • L'ID du projet est unique parmi tous les projets Google Cloud et non modifiable une fois défini. La console Cloud génère automatiquement une chaîne unique (en général, vous n'y accordez d'importance particulière). Dans la plupart des ateliers de programmation, vous devrez indiquer l'ID de votre projet (généralement identifié par PROJECT_ID). Si l'ID généré ne vous convient pas, vous pouvez en générer un autre de manière aléatoire. Vous pouvez également en spécifier un et voir s'il est disponible. Après cette étape, l'ID n'est plus modifiable et restera donc le même pour toute la durée du projet.
  • Pour information, il existe une troisième valeur (le numéro de projet) que certaines API utilisent. Pour en savoir plus sur ces trois valeurs, consultez la documentation.
  1. Vous devez ensuite activer la facturation dans la console Cloud pour utiliser les ressources/API Cloud. L'exécution de cet atelier de programmation est très peu coûteuse, voire sans frais. Pour désactiver les ressources et éviter ainsi que des frais ne vous soient facturés après ce tutoriel, vous pouvez supprimer le projet ou les ressources que vous avez créées. Les nouveaux utilisateurs de Google Cloud peuvent participer au programme d'essai sans frais pour bénéficier d'un crédit de 300 $.

Démarrer Cloud Shell

Bien que Google Cloud puisse être utilisé à distance depuis votre ordinateur portable, nous allons nous servir de Google Cloud Shell pour cet atelier de programmation, un environnement de ligne de commande exécuté dans le cloud.

Dans la console Google Cloud, cliquez sur l'icône Cloud Shell dans la barre d'outils supérieure :

84688aa223b1c3a2.png

Le provisionnement et la connexion à l'environnement prennent quelques instants seulement. Une fois l'opération terminée, le résultat devrait ressembler à ceci :

320e18fedb7fbe0.png

Cette machine virtuelle contient tous les outils de développement nécessaires. Elle comprend un répertoire d'accueil persistant de 5 Go et s'exécute sur Google Cloud, ce qui améliore nettement les performances du réseau et l'authentification. Vous pouvez effectuer toutes les tâches de cet atelier de programmation dans un navigateur. Vous n'avez rien à installer.

3. Préparer l'environnement et activer les API Cloud

Pour utiliser les différents services dont nous aurons besoin tout au long de ce projet, nous allons activer quelques API. Pour ce faire, nous allons lancer la commande suivante dans Cloud Shell:

$ gcloud services enable \
      appengine.googleapis.com \
      cloudbuild.googleapis.com \
      cloudfunctions.googleapis.com \
      compute.googleapis.com \
      firestore.googleapis.com \
      run.googleapis.com

Après un certain temps, l'opération doit se terminer correctement:

Operation "operations/acf.5c5ef4f6-f734-455d-b2f0-ee70b5a17322" finished successfully.

Nous allons également configurer une variable d'environnement dont nous aurons besoin en cours de route: la région cloud dans laquelle nous déploierons la fonction, l'application et le conteneur:

$ export REGION=europe-west3

Étant donné que nous allons stocker des données dans la base de données Cloud Firestore, nous devons créer la base de données:

$ gcloud app create --region=${REGION}
$ gcloud firestore databases create --location=${REGION}

Plus tard dans cet atelier de programmation, lors de l'implémentation de l'API REST, nous devrons trier et filtrer les données. À cette fin, nous allons créer trois index:

$ gcloud firestore indexes composite create --collection-group=books \
      --field-config field-path=language,order=ascending \
      --field-config field-path=updated,order=descending 

$ gcloud firestore indexes composite create --collection-group=books \
      --field-config field-path=author,order=ascending \
      --field-config field-path=updated,order=descending 

Ces trois index correspondent à des recherches que nous effectuerons par auteur ou par langue, tout en maintenant l'ordre dans la collection grâce à un champ mis à jour.

4. Obtenir le code

Récupérez le code à partir du dépôt GitHub suivant:

$ git clone https://github.com/glaforge/serverless-web-apis

Le code de l'application est écrit à l'aide de Node.JS.

La structure de dossiers suivante est pertinente pour cet atelier:

serverless-web-apis
 |
 ├── data
 |   ├── books.json
 |
 ├── function-import
 |   ├── index.js
 |   ├── package.json
 |
 ├── run-crud
 |   ├── index.js
 |   ├── package.json
 |   ├── Dockerfile
 |
 ├── appengine-frontend
     ├── public
     |   ├── css/style.css
     |   ├── html/index.html
     |   ├── js/app.js
     ├── index.js
     ├── package.json
     ├── app.yaml

Voici les dossiers concernés:

  • data : ce dossier contient un échantillon de données d'une liste de 100 livres.
  • function-import : cette fonction propose un point de terminaison pour importer des exemples de données.
  • run-crud : ce conteneur exposera une API Web pour accéder aux données de livres stockées dans Cloud Firestore.
  • appengine-frontend : cette application Web App Engine affiche une interface simple en lecture seule permettant de parcourir la liste des livres.

5. Données de la bibliothèque de livres

Dans le dossier de données, nous avons un fichier books.json qui contient une liste d'une centaine de livres, qu'il peut être utile de lire. Ce document JSON est un tableau contenant des objets JSON. Examinons la forme des données que nous allons ingérer via une fonction Cloud:

[
  {
    "isbn": "9780435272463",
    "author": "Chinua Achebe",
    "language": "English",
    "pages": 209,
    "title": "Things Fall Apart",
    "year": 1958
  },
  {
    "isbn": "9781414251196",
    "author": "Hans Christian Andersen",
    "language": "Danish",
    "pages": 784,
    "title": "Fairy tales",
    "year": 1836
  },
  ...
]

Toutes les entrées de livre de ce tableau contiennent les informations suivantes:

  • isbn : code ISBN-13 identifiant le livre.
  • author : nom de l'auteur du livre.
  • language : langue dans laquelle le livre est écrit.
  • pages : nombre de pages du livre.
  • title : titre du livre.
  • year : année de publication du livre

6. Point de terminaison de fonction permettant d'importer des exemples de données de livre

Dans cette première section, nous allons implémenter le point de terminaison qui sera utilisé pour importer des exemples de données de livres. Pour cela, nous utiliserons Cloud Functions.

Explorer le code

Commençons par examiner le fichier package.json:

{
    "name": "function-import",
    "description": "Import sample book data",
    "license": "Apache-2.0",
    "dependencies": {
        "@google-cloud/firestore": "^4.9.9"
    },
    "devDependencies": {
        "@google-cloud/functions-framework": "^3.1.0"
    },
    "scripts": {
        "start": "npx @google-cloud/functions-framework --target=parseBooks"
    }
}

Dans les dépendances de l'environnement d'exécution, nous n'avons besoin que du module NPM @google-cloud/firestore pour accéder à la base de données et stocker les données de nos livres. En arrière-plan, l'environnement d'exécution Cloud Functions fournit également le framework Web Express. Il n'est donc pas nécessaire de le déclarer en tant que dépendance.

Dans les dépendances de développement, nous déclarons le framework des fonctions (@google-cloud/functions-framework), qui est le framework d'exécution utilisé pour appeler vos fonctions. Il s'agit d'un framework Open Source que vous pouvez également utiliser localement sur votre machine (dans notre cas, dans Cloud Shell) pour exécuter des fonctions sans les déployer à chaque modification, ce qui améliore la boucle de rétroaction du développement.

Pour installer les dépendances, exécutez la commande install:

$ npm install

Le script start utilise le framework des fonctions pour vous fournir une commande que vous pouvez exécuter en local à l'aide de l'instruction suivante:

$ npm start

Vous pouvez utiliser curl ou éventuellement l'aperçu sur le Web de Cloud Shell pour que les requêtes HTTP GET puissent interagir avec la fonction.

Examinons maintenant le fichier index.js qui contient la logique de notre fonction d'importation de données de livres:

const Firestore = require('@google-cloud/firestore');
const firestore = new Firestore();
const bookStore = firestore.collection('books');

Nous instancions le module Firestore et nous pointons vers la collection de livres (comme une table dans des bases de données relationnelles).

functions.http('parseBooks', async (req, resp) => {
    if (req.method !== "POST") {
        resp.status(405).send({error: "Only method POST allowed"});
        return;
    }
    if (req.headers['content-type'] !== "application/json") {
        resp.status(406).send({error: "Only application/json accepted"});
        return;
    }
    ... 
})

Nous exportons la fonction JavaScript parseBooks. C'est la fonction que nous déclarerons lors du déploiement ultérieur.

Les deux instructions suivantes consistent à vérifier que:

  • Nous n'acceptons que les requêtes HTTP POST. Sinon, renvoyez un code d'état 405 pour indiquer que les autres méthodes HTTP ne sont pas autorisées.
  • Nous n'acceptons que les charges utiles application/json. Par ailleurs, nous envoyons un code d'état 406 pour indiquer qu'il ne s'agit pas d'un format de charge utile acceptable.
    const books = req.body;

    const writeBatch = firestore.batch();

    for (const book of books) {
        const doc = bookStore.doc(book.isbn);
        writeBatch.set(doc, {
            title: book.title,
            author: book.author,
            language: book.language,
            pages: book.pages,
            year: book.year,
            updated: Firestore.Timestamp.now()
        });
    }

Nous pouvons ensuite récupérer la charge utile JSON via le body de la requête. Nous préparons une opération par lot Firestore afin de stocker tous les livres de manière groupée. Nous effectuons une itération sur le tableau JSON, qui contient les détails du livre, en passant par les champs isbn, title, author, language, pages et year. Le code ISBN du livre sert de clé primaire ou d'identifiant.

    try {
        await writeBatch.commit();
        console.log("Saved books in Firestore");
    } catch (e) {
        console.error("Error saving books:", e);
        resp.status(400).send({error: "Error saving books"});
        return;
    };

    resp.status(202).send({status: "OK"});

Maintenant que la majeure partie des données est prête, nous pouvons valider l'opération. Si l'opération de stockage échoue, nous renvoyons un code d'état 400 pour indiquer l'échec. Sinon, nous pouvons renvoyer une réponse OK, avec un code d'état 202 indiquant que la demande d'enregistrement groupé a été acceptée.

Exécuter et tester la fonction d'importation

Avant d'exécuter le code, nous allons installer les dépendances avec la commande suivante:

$ npm install

Pour exécuter la fonction localement grâce au framework des fonctions, nous allons utiliser la commande de script start que nous avons définie dans package.json:

$ npm start

> start
> npx @google-cloud/functions-framework --target=parseBooks

Serving function...
Function: parseBooks
URL: http://localhost:8080/

Pour envoyer une requête HTTP POST à votre fonction locale, vous pouvez exécuter la commande suivante:

$ curl -d "@../data/books.json" \
       -H "Content-Type: application/json" \
       http://localhost:8080/

Lorsque vous lancez cette commande, le résultat suivant s'affiche, confirmant que la fonction s'exécute en local:

{"status":"OK"}

Vous pouvez également accéder à l'interface utilisateur de la console Cloud pour vérifier que les données sont bien stockées dans Firestore:

409982568cebdbf8.png

La capture d'écran ci-dessus montre la collection books créée, la liste des documents de livre identifiés par le code ISBN du livre et les détails de l'entrée correspondante sur la droite.

Déployer la fonction dans le cloud

Pour déployer la fonction dans Cloud Functions, nous allons utiliser la commande suivante dans le répertoire function-import:

$ gcloud functions deploy bulk-import \
         --gen2 \
         --trigger-http \
         --runtime=nodejs20 \
         --allow-unauthenticated \
         --max-instances=30
         --region=${REGION} \
         --source=. \
         --entry-point=parseBooks

Nous déployons la fonction avec le nom symbolique bulk-import. Cette fonction est déclenchée via des requêtes HTTP. Nous utilisons l'environnement d'exécution Node.JS 20. Nous déployons la fonction publiquement (idéalement, nous devrions sécuriser ce point de terminaison). Nous spécifions la région dans laquelle nous voulons que la fonction réside. Nous pointons également les sources dans le répertoire local et utilisons parseBooks (la fonction JavaScript exportée) comme point d'entrée.

Après quelques minutes ou moins, la fonction est déployée dans le cloud. Dans l'interface utilisateur de la console Cloud, la fonction doit s'afficher:

c910875d4dc0aaa8.png

Dans le résultat du déploiement, vous devriez pouvoir voir l'URL de votre fonction, qui suit une certaine convention d'attribution de noms (https://${REGION}-${GOOGLE_CLOUD_PROJECT}.cloudfunctions.net/${FUNCTION_NAME}). Bien entendu, vous trouverez également cette URL de déclencheur HTTP dans l'interface utilisateur de la console Cloud, dans l'onglet du déclencheur:

380ffc46eb56441e.png

Vous pouvez également récupérer l'URL via la ligne de commande à l'aide de gcloud:

$ export BULK_IMPORT_URL=$(gcloud functions describe bulk-import \
                                  --region=$REGION \
                                  --format 'value(httpsTrigger.url)')
$ echo $BULK_IMPORT_URL

Nous allons le stocker dans la variable d'environnement BULK_IMPORT_URL afin de pouvoir la réutiliser pour tester la fonction déployée.

Tester la fonction déployée

À l'aide d'une commande curl que nous avons déjà utilisée pour tester la fonction exécutée localement, nous allons tester la fonction déployée. La seule modification concerne l'URL:

$ curl -d "@../data/books.json" \
       -H "Content-Type: application/json" \
       $BULK_IMPORT_URL

Là encore, si l'opération réussit, le résultat suivant doit s'afficher:

{"status":"OK"}

Maintenant que notre fonction d'importation est déployée et prête, que nous avons importé nos exemples de données, il est temps de développer l'API REST pour exposer cet ensemble de données.

7. Contrat de l'API REST

Même si nous ne définissons pas de contrat d'API utilisant, par exemple, la spécification OpenAPI, nous allons examiner les différents points de terminaison de notre API REST.

L'API échange des objets JSON, constitués des éléments suivants:

  • isbn (facultatif) : String à 13 caractères représentant un code ISBN valide.
  • author : un String non vide représentant le nom de l'auteur du livre.
  • language : un String non vide contenant la langue dans laquelle le livre a été écrit.
  • pages : valeur Integer positive correspondant au nombre de pages du livre
  • title : String non vide avec le titre du livre
  • year : valeur Integer pour l'année de publication du livre.

Exemple de charge utile de livre:

{
    "isbn": "9780435272463",
    "author": "Chinua Achebe",
    "language": "English",
    "pages": 209,
    "title": "Things Fall Apart",
    "year": 1958
  }

OBTENIR /books

Obtenez la liste de tous les livres, potentiellement filtrée par auteur et/ou langue, paginée par fenêtres de 10 résultats à la fois.

Charge utile du corps: aucune.

Paramètres de requête:

  • author (facultatif) : filtre la liste de livres par auteur.
  • language (facultatif) : filtre la liste de livres par langue.
  • page (facultatif, valeur par défaut = 0) : indique le classement de la page de résultats à renvoyer.

Renvoie un tableau JSON d'objets livre.

Codes d'état:

  • 200 : lorsque la requête réussit à récupérer la liste des livres,
  • 400 : si une erreur se produit

POST /books et POST /books/{isbn}

Publier une nouvelle charge utile de livre, avec un paramètre de chemin d'accès isbn (auquel cas le code isbn n'est pas nécessaire dans la charge utile du livre) ou sans (auquel cas le code isbn doit être présent dans la charge utile du livre)

Charge utile du corps: objet de livre.

Paramètres de requête: aucun.

Renvoie: rien.

Codes d'état:

  • 201 : lorsque le livre est correctement stocké,
  • 406 : si le code isbn n'est pas valide,
  • 400 : si une erreur se produit

GET /books/{isbn}

Récupère un livre de la bibliothèque, identifié par son code isbn, transmis en tant que paramètre de chemin.

Charge utile du corps: aucune.

Paramètres de requête: aucun.

Renvoie un objet JSON de livre ou un objet d'erreur si le livre n'existe pas.

Codes d'état:

  • 200 : si le livre est trouvé dans la base de données,
  • 400 : si une erreur se produit,
  • 404 : si le livre est introuvable,
  • 406 : si le code isbn n'est pas valide.

PUT /books/{isbn}

Met à jour un livre existant, identifié par son isbn transmis en tant que paramètre de chemin.

Charge utile du corps: objet de livre. Seuls les champs nécessitant une mise à jour peuvent être transmis, les autres étant facultatifs.

Paramètres de requête: aucun.

Renvoie le livre mis à jour.

Codes d'état:

  • 200 : lorsque le livre est correctement mis à jour,
  • 400 : si une erreur se produit,
  • 406 : si le code isbn n'est pas valide.

SUPPRIMER /books/{isbn}

Supprime un livre existant, identifié par son isbn transmis en tant que paramètre de chemin.

Charge utile du corps: aucune.

Paramètres de requête: aucun.

Renvoie: rien.

Codes d'état:

  • 204 : lorsque le livre est supprimé,
  • 400 : si une erreur se produit

8. Déployer et exposer une API REST dans un conteneur

Explorer le code

Dockerfile

Commençons par examiner Dockerfile, qui sera responsable de la conteneurisation de notre code d'application:

FROM node:20-slim
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install --only=production
COPY . ./
CMD [ "node", "index.js" ]

Nous utilisons une image "slim" Node.JS 20. Nous travaillons dans le répertoire /usr/src/app. Nous copions actuellement le fichier package.json (voir ci-dessous) qui définit nos dépendances, entre autres. Nous installons les dépendances avec npm install en copiant le code source. Enfin, nous indiquons comment exécuter cette application à l'aide de la commande node index.js.

package.json

Nous pouvons ensuite examiner le fichier package.json:

{
    "name": "run-crud",
    "description": "CRUD operations over book data",
    "license": "Apache-2.0",
    "engines": {
        "node": ">= 20.0.0"
    },
    "dependencies": {
        "@google-cloud/firestore": "^4.9.9",
        "cors": "^2.8.5",
        "express": "^4.17.1",
        "isbn3": "^1.1.10"
    },
    "scripts": {
        "start": "node index.js"
    }
}

Nous spécifions que nous voulons utiliser Node.JS 14, comme ce fut le cas avec Dockerfile.

Notre application API Web dépend des éléments suivants:

  • Le module NPM Firestore pour accéder aux données de livres de la base de données
  • La bibliothèque cors pour gérer les requêtes CORS (Cross Origin Resource Sharing), car notre API REST sera appelée depuis le code client de l'interface de notre application Web App Engine
  • Le framework Express, qui sera notre framework Web pour concevoir notre API,
  • Ensuite, le module isbn3, qui aide à valider les codes ISBN des livres.

Nous spécifions également le script start, qui sera utile pour démarrer l'application en local, à des fins de développement et de test.

index.js

Passons à l'essentiel du code, en examinant plus en détail index.js:

const Firestore = require('@google-cloud/firestore');
const firestore = new Firestore();
const bookStore = firestore.collection('books');

Nous avons besoin du module Firestore et référençons la collection books, dans laquelle les données de nos livres sont stockées.

const express = require('express');
const app = express();
const bodyParser = require('body-parser');
app.use(bodyParser.json());

const querystring = require('querystring');

const cors = require('cors');
app.use(cors({
    exposedHeaders: ['Content-Length', 'Content-Type', 'Link'],
}));

Nous utilisons Express comme framework Web pour implémenter notre API REST. Nous utilisons le module body-parser pour analyser les charges utiles JSON échangées avec notre API.

Le module querystring est utile pour manipuler les URL. Ce sera le cas lorsque nous créerons des en-têtes Link à des fins de pagination (nous y reviendrons plus tard).

Ensuite, nous configurons le module cors. Nous expliquons les en-têtes à transmettre via CORS, car la plupart sont généralement supprimés, mais dans le cas présent, nous voulons conserver la longueur et le type de contenu habituels, ainsi que l'en-tête Link que nous spécifierons pour la pagination.

const ISBN = require('isbn3');

function isbnOK(isbn, res) {
    const parsedIsbn = ISBN.parse(isbn);
    if (!parsedIsbn) {
        res.status(406)
            .send({error: `Invalid ISBN: ${isbn}`});
        return false;
    }
    return parsedIsbn;
}

Nous allons utiliser le module NPM isbn3 pour analyser et valider les codes ISBN. Nous développons également une petite fonction utilitaire qui analyse les codes ISBN et renvoie un code d'état 406 dans la réponse, si les codes ISBN ne sont pas valides.

  • GET /books

Examinons le point de terminaison GET /books, élément par élément:

app.get('/books', async (req, res) => {
    try {
        var query = new Firestore().collection('books');

        if (!!req.query.author) {
            console.log(`Filtering by author: ${req.query.author}`);
            query = query.where("author", "==", req.query.author);
        }
        if (!!req.query.language) {
            console.log(`Filtering by language: ${req.query.language}`);
            query = query.where("language", "==", req.query.language);
        }

        const page = parseInt(req.query.page) || 0;

        // - -  - -  - -  - -  - -  - -

    } catch (e) {
        console.error('Failed to fetch books', e);
        res.status(400)
            .send({error: `Impossible to fetch books: ${e.message}`});
    }
});

Nous nous préparons à interroger la base de données en préparant une requête. Cette requête dépend des paramètres de requête facultatifs, pour filtrer par auteur et/ou par langue. Nous renvoyons également la liste des livres par morceaux de 10 livres.

Si une erreur se produit lors de la récupération des livres, nous affichons une erreur avec un code d'état 400.

Zoomons sur la partie coupée de ce point d'extrémité:

        const snapshot = await query
            .orderBy('updated', 'desc')
            .limit(PAGE_SIZE)
            .offset(PAGE_SIZE * page)
            .get();

        const books = [];

        if (snapshot.empty) {
            console.log('No book found');
        } else {
            snapshot.forEach(doc => {
                const {title, author, pages, year, language, ...otherFields} = doc.data();
                const book = {isbn: doc.id, title, author, pages, year, language};
                books.push(book);
            });
        }

Dans la section précédente, nous avons filtré par author et language, mais dans cette section, nous allons trier la liste des livres par ordre de la date de dernière mise à jour (la dernière mise à jour intervient en premier). Nous allons également paginer le résultat en définissant une limite (le nombre d'éléments à renvoyer) et un décalage (le point de départ à partir duquel renvoyer le lot de livres suivant).

Nous exécutons la requête, obtenons un instantané des données et plaçons ces résultats dans un tableau JavaScript qui sera renvoyé à la fin de la fonction.

Terminons les explications de ce point de terminaison en suivant une bonne pratique: utiliser l'en-tête Link pour définir des liens d'URI vers la première, la précédente, la suivante ou la dernière page de données (dans notre cas, nous ne fournirons que la précédente et la suivante).

        var links = {};
        if (page > 0) {
            const prevQuery = querystring.stringify({...req.query, page: page - 1});
            links.prev = `${req.path}${prevQuery != '' ? `?${prevQuery}` : ''}`;
        }
        if (snapshot.docs.length === PAGE_SIZE) {
            const nextQuery = querystring.stringify({...req.query, page: page + 1});
            links.next = `${req.path}${nextQuery != '' ? `?${nextQuery}` : ''}`;
        }
        if (Object.keys(links).length > 0) {
            res.links(links);
        }

        res.status(200).send(books);

La logique peut sembler un peu complexe dans ce cas, mais nous allons ajouter un lien Précédent si nous ne sommes pas sur la première page de données. Nous ajoutons également un lien Suivant si la page de données est pleine (c'est-à-dire si elle contient le nombre maximal de livres défini par la constante PAGE_SIZE, en supposant qu'un autre lien fournit davantage de données). Nous utilisons ensuite la fonction resource#links() d'Express pour créer l'en-tête approprié avec la syntaxe appropriée.

Pour votre information, l'en-tête du lien se présente comme suit:

link: </books?page=1>; rel="prev", </books?page=3>; rel="next"
  • POST /books et POST /books/:isbn

Ces deux points de terminaison permettent de créer un livre. L'un transmet le code ISBN dans la charge utile du livre, tandis que l'autre le transmet en tant que paramètre de chemin. Dans tous les cas, les deux appellent notre fonction createBook():

async function createBook(isbn, req, res) {
    const parsedIsbn = isbnOK(isbn, res);
    if (!parsedIsbn) return;

    const {title, author, pages, year, language} = req.body;

    try {
        const docRef = bookStore.doc(parsedIsbn.isbn13);
        await docRef.set({
            title, author, pages, year, language,
            updated: Firestore.Timestamp.now()
        });
        console.log(`Saved book ${parsedIsbn.isbn13}`);

        res.status(201)
            .location(`/books/${parsedIsbn.isbn13}`)
            .send({status: `Book ${parsedIsbn.isbn13} created`});
    } catch (e) {
        console.error(`Failed to save book ${parsedIsbn.isbn13}`, e);
        res.status(400)
            .send({error: `Impossible to create book ${parsedIsbn.isbn13}: ${e.message}`});
    }    
}

Nous vérifions que le code isbn est valide. Sinon, nous le renvoyons à partir de la fonction (et en définissant un code d'état 406). Nous récupérons les champs de livre à partir de la charge utile transmise dans le corps de la requête. Ensuite, nous allons stocker les détails des livres dans Firestore. Renvoi de 201 en cas de réussite et de 400 en cas d'échec.

Lors du renvoi, nous définissons également l'en-tête "location" afin de fournir des indications au client de l'API où se trouve la ressource nouvellement créée. L'en-tête ressemblera à ceci:

Location: /books/9781234567898
  • GET /books/:isbn

récupérons un livre, identifié par son ISBN, dans Firestore.

app.get('/books/:isbn', async (req, res) => {
    const parsedIsbn = isbnOK(req.params.isbn, res);
    if (!parsedIsbn) return;

    try {
        const docRef = bookStore.doc(parsedIsbn.isbn13);
        const docSnapshot = await docRef.get();

        if (!docSnapshot.exists) {
            console.log(`Book not found ${parsedIsbn.isbn13}`)
            res.status(404)
                .send({error: `Could not find book ${parsedIsbn.isbn13}`});
            return;
        }

        console.log(`Fetched book ${parsedIsbn.isbn13}`, docSnapshot.data());

        const {title, author, pages, year, language, ...otherFields} = docSnapshot.data();
        const book = {isbn: parsedIsbn.isbn13, title, author, pages, year, language};

        res.status(200).send(book);
    } catch (e) {
        console.error(`Failed to fetch book ${parsedIsbn.isbn13}`, e);
        res.status(400)
            .send({error: `Impossible to fetch book ${parsedIsbn.isbn13}: ${e.message}`});
    }
});

Comme toujours, nous vérifions si l'ISBN est valide. Nous envoyons une requête à Firestore pour récupérer le livre. La propriété snapshot.exists est pratique pour savoir si un livre a été trouvé. Sinon, nous renvoyons une erreur et le code d'état 404 Not Found. Nous récupérons les données du livre et créons un objet JSON représentant le livre, qui sera renvoyé.

  • PUT /books/:isbn

Nous utilisons la méthode PUT pour mettre à jour un livre existant.

app.put('/books/:isbn', async (req, res) => {
    const parsedIsbn = isbnOK(req.params.isbn, res);
    if (!parsedIsbn) return;

    try {
        const docRef = bookStore.doc(parsedIsbn.isbn13);
        await docRef.set({
            ...req.body,
            updated: Firestore.Timestamp.now()
        }, {merge: true});
        console.log(`Updated book ${parsedIsbn.isbn13}`);

        res.status(201)
            .location(`/books/${parsedIsbn.isbn13}`)
            .send({status: `Book ${parsedIsbn.isbn13} updated`});
    } catch (e) {
        console.error(`Failed to update book ${parsedIsbn.isbn13}`, e);
        res.status(400)
            .send({error: `Impossible to update book ${parsedIsbn.isbn13}: ${e.message}`});
    }    
});

Nous mettons à jour le champ de date/heure updated pour qu'il se souvienne de la dernière mise à jour de cet enregistrement. Nous utilisons la stratégie {merge:true}, qui remplace les champs existants par leurs nouvelles valeurs (sinon, tous les champs sont supprimés et seuls les nouveaux champs de la charge utile seront enregistrés, ce qui efface les champs existants de la mise à jour précédente ou de la création initiale).

Nous définissons également l'en-tête Location pour qu'il pointe vers l'URI du livre.

  • DELETE /books/:isbn

Il est assez simple de supprimer des livres. Il nous suffit d'appeler la méthode delete() sur la référence du document. Nous renvoyons un code d'état 204, car nous ne renvoyons aucun contenu.

app.delete('/books/:isbn', async (req, res) => {
    const parsedIsbn = isbnOK(req.params.isbn, res);
    if (!parsedIsbn) return;

    try {
        const docRef = bookStore.doc(parsedIsbn.isbn13);
        await docRef.delete();
        console.log(`Book ${parsedIsbn.isbn13} was deleted`);

        res.status(204).end();
    } catch (e) {
        console.error(`Failed to delete book ${parsedIsbn.isbn13}`, e);
        res.status(400)
            .send({error: `Impossible to delete book ${parsedIsbn.isbn13}: ${e.message}`});
    }
});

Démarrer le serveur Express / Node

Enfin et surtout, nous démarrons le serveur en écoutant le port 8080 par défaut:

const port = process.env.PORT || 8080;
app.listen(port, () => {
    console.log(`Books Web API service: listening on port ${port}`);
    console.log(`Node ${process.version}`);
});

Exécuter votre application en local

Pour exécuter l'application en local, nous allons d'abord installer les dépendances avec la commande suivante:

$ npm install

Nous pouvons alors commencer par:

$ npm start

Le serveur démarrera sur localhost et écoutera sur le port 8080 par défaut.

Vous pouvez également créer un conteneur Docker et exécuter l'image de conteneur à l'aide des commandes suivantes:

$ docker build -t crud-web-api .

$ docker run --rm -p 8080:8080 -it crud-web-api

L'exécution dans Docker est également un excellent moyen de vérifier que la conteneurisation de notre application fonctionne correctement lorsque nous la compilons dans le cloud avec Cloud Build.

Tester l'API

Quelle que soit la façon dont nous exécutons le code de l'API REST (directement via le nœud ou via une image de conteneur Docker), nous sommes désormais en mesure d'exécuter quelques requêtes sur celui-ci.

  • Créer un livre (ISBN dans la charge utile du corps):
$ curl -XPOST -d '{"isbn":"9782070368228","title":"Book","author":"me","pages":123,"year":2021,"language":"French"}' \
    -H "Content-Type: application/json" \
    http://localhost:8080/books
  • Créer un livre (ISBN dans un paramètre de chemin d'accès):
$ curl -XPOST -d '{"title":"Book","author":"me","pages":123,"year":2021,"language":"French"}' \
    -H "Content-Type: application/json" \
    http://localhost:8080/books/9782070368228
  • Supprimer un livre (celui que nous avons créé):
$ curl -XDELETE http://localhost:8080/books/9782070368228
  • Récupérer un livre par le biais d'un ISBN:
$ curl http://localhost:8080/books/9780140449136
$ curl http://localhost:8080/books/9782070360536
  • Mettez à jour un livre existant en ne modifiant que son titre:
$ curl -XPUT \
       -d '{"title":"Book"}' \
       -H "Content-Type: application/json" \      
       http://localhost:8080/books/9780003701203
  • Récupérez la liste des livres (les 10 premiers):
$ curl http://localhost:8080/books
  • Recherchez les livres écrits par un auteur particulier:
$ curl http://localhost:8080/books?author=Virginia+Woolf
  • Dressez la liste des livres écrits en anglais:
$ curl http://localhost:8080/books?language=English
  • Chargez la quatrième page de livres:
$ curl http://localhost:8080/books?page=3

Nous pouvons également combiner les paramètres de requête author, language et books pour affiner notre recherche.

Créer et déployer l'API REST conteneurisée

Nous sommes ravis que l'API REST fonctionne comme prévu. C'est donc le moment idéal pour la déployer dans le cloud, sur Cloud Run.

Nous allons le faire en deux étapes:

  • Commencez par créer l'image de conteneur à l'aide de Cloud Build, à l'aide de la commande suivante:
$ gcloud builds submit \
         --tag gcr.io/${GOOGLE_CLOUD_PROJECT}/crud-web-api
  • Ensuite, en déployant le service à l'aide de cette deuxième commande:
$ gcloud run deploy run-crud \
         --image gcr.io/${GOOGLE_CLOUD_PROJECT}/crud-web-api \
         --allow-unauthenticated \
         --region=${REGION} \
         --platform=managed

Avec la première commande, Cloud Build crée l'image de conteneur et l'héberge dans Container Registry. La commande suivante déploie l'image de conteneur à partir du registre et la déploie dans la région cloud.

Nous pouvons vérifier dans l'UI de la console Cloud que notre service Cloud Run figure désormais dans la liste:

f62fbca02a8127c0.png

La dernière étape consiste à récupérer l'URL du service Cloud Run récemment déployé à l'aide de la commande suivante:

$ export RUN_CRUD_SERVICE_URL=$(gcloud run services describe run-crud \
                                       --region=${REGION} \
                                       --platform=managed \
                                       --format='value(status.url)')

Nous aurons besoin de l'URL de notre API REST Cloud Run dans la section suivante, car le code de l'interface App Engine va interagir avec l'API.

9. Héberger une application Web pour parcourir la bibliothèque

La dernière partie du puzzle pour ajouter un peu de paillettes à ce projet consiste à fournir une interface Web qui interagira avec notre API REST. À cette fin, nous utiliserons Google App Engine, avec du code JavaScript client qui appelle l'API via des requêtes AJAX (à l'aide de l'API Fetch côté client).

Bien que déployée dans l'environnement d'exécution App Engine Node.JS, notre application est principalement composée de ressources statiques. Il n'y a pas beaucoup de code backend, car la majeure partie de l'interaction utilisateur se fera dans le navigateur via un code JavaScript côté client. Nous n'allons pas utiliser un framework JavaScript d'interface sophistiqué. Nous utiliserons simplement du code JavaScript vanilla, avec quelques composants Web pour l'interface utilisateur à l'aide de la bibliothèque de composants Web Shoelace:

  • une zone de sélection pour choisir la langue du livre:

6fb9f741000a2dc1.png

  • Un composant de carte pour afficher des informations sur un livre particulier (y compris un code-barres représentant l'ISBN du livre, à l'aide de la bibliothèque JsBarcode):

3aa21a9e16e3244e.png

  • et un bouton pour charger d'autres livres à partir de la base de données:

3925ad81c91bbac9.png

Lorsque l'on combine tous ces composants visuels, la page Web obtenue pour parcourir notre bibliothèque se présente comme suit:

18a5117150977d6.png

Le fichier de configuration app.yaml

Commençons par explorer le code base de cette application App Engine en examinant son fichier de configuration app.yaml. Il s'agit d'un fichier spécifique à App Engine. Il permet de configurer des éléments tels que les variables d'environnement et les différents "gestionnaires" de l'application, ou de spécifier que certaines ressources sont des éléments statiques qui seront diffusés par le CDN intégré d'App Engine.

runtime: nodejs14

env_variables:
  RUN_CRUD_SERVICE_URL: CHANGE_ME

handlers:

- url: /js
  static_dir: public/js

- url: /css
  static_dir: public/css

- url: /img
  static_dir: public/img

- url: /(.+\.html)
  static_files: public/html/\1
  upload: public/(.+\.html)

- url: /
  static_files: public/html/index.html
  upload: public/html/index\.html

- url: /.*
  secure: always
  script: auto

Nous spécifions que notre application est de type Node.JS et que nous souhaitons utiliser la version 14.

Nous définissons ensuite une variable d'environnement qui pointe vers l'URL de notre service Cloud Run. Nous devons remplacer l'espace réservé "CHANGE_ME" par l'URL correcte (reportez-vous à la section ci-dessous pour savoir comment modifier ce paramètre).

Ensuite, nous définissons différents gestionnaires. Les trois premières pointent vers l'emplacement du code côté client HTML, CSS et JavaScript, sous le dossier public/ et ses sous-dossiers. Le quatrième indique que l'URL racine de l'application App Engine doit renvoyer vers la page index.html. Ainsi, le suffixe index.html ne s'affichera pas dans l'URL lors de l'accès à la racine du site Web. La dernière est l'URL par défaut qui achemine toutes les autres URL (/.*) vers notre application Node.JS (c'est-à-dire la partie "dynamique" de l'application, contrairement aux éléments statiques que nous avons décrits).

Nous allons maintenant mettre à jour l'URL de l'API Web du service Cloud Run.

Dans le répertoire appengine-frontend/, exécutez la commande suivante pour mettre à jour la variable d'environnement pointant vers l'URL de notre API REST basée sur Cloud Run:

$ sed -i -e "s|CHANGE_ME|${RUN_CRUD_SERVICE_URL}|" app.yaml

Vous pouvez également modifier manuellement la chaîne CHANGE_ME dans app.yaml par l'URL appropriée:

env_variables:
    RUN_CRUD_SERVICE_URL: CHANGE_ME

Le fichier package.json Node.JS

{
    "name": "appengine-frontend",
    "description": "Web frontend",
    "license": "Apache-2.0",
    "main": "index.js",
    "engines": {
        "node": "^14.0.0"
    },
    "dependencies": {
        "express": "^4.17.1",
        "isbn3": "^1.1.10"
    },
    "devDependencies": {
        "nodemon": "^2.0.7"
    },
    "scripts": {
        "start": "node index.js",
        "dev": "nodemon --watch server --inspect index.js"
    }
}

Nous insistons encore une fois sur le fait que nous voulons exécuter cette application à l'aide de Node.js 14. Nous dépendons du framework Express, ainsi que du module NPM isbn3 pour valider les livres Codes ISBN

Dans les dépendances de développement, nous allons utiliser le module nodemon pour surveiller les modifications apportées aux fichiers. Bien que nous puissions exécuter notre application localement avec npm start, apporter quelques modifications au code, arrêter l'application avec ^C, puis la relancer, cela peut s'avérer un peu fastidieux. À la place, nous pouvons utiliser la commande suivante pour que l'application soit automatiquement rechargée / redémarrée en cas de modification:

$ npm run dev

Le code Node.JS index.js

const express = require('express');
const app = express();

app.use(express.static('public'));

const bodyParser = require('body-parser');
app.use(bodyParser.json());

Nous avons besoin du framework Web Express. Nous indiquons que le répertoire public contient des éléments statiques pouvant être diffusés (au moins lors d'une exécution locale en mode de développement) par le middleware static. Enfin, nous demandons à body-parser d'analyser nos charges utiles JSON.

Examinons les deux routes que nous avons définies:

app.get('/', async (req, res) => {
    res.redirect('/html/index.html');
});

app.get('/webapi', async (req, res) => {
    res.send(process.env.RUN_CRUD_SERVICE_URL);
});

Le premier / correspondant sera redirigé vers index.html dans notre répertoire public/html. Comme en mode développement, nous ne nous exécutons pas dans l'environnement d'exécution App Engine. Par conséquent, le routage d'URL d'App Engine n'a pas lieu. Au lieu de cela, ici, nous redirigeons simplement l'URL racine vers le fichier HTML.

Le deuxième point de terminaison que nous définissons /webapi renvoie l'URL de notre API REST Cloud RUN. Ainsi, le code JavaScript côté client saura où appeler pour obtenir la liste des livres.

const port = process.env.PORT || 8080;
app.listen(port, () => {
    console.log(`Book library web frontend: listening on port ${port}`);
    console.log(`Node ${process.version}`);
    console.log(`Web API endpoint ${process.env.RUN_CRUD_SERVICE_URL}`);
});

Pour terminer, nous exécutons l'application Web Express et nous écoute sur le port 8080 par défaut.

Page index.html

Nous n'examinerons pas chaque ligne de cette longue page HTML. Au lieu de cela, soulignons quelques lignes clés.

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.37/dist/themes/base.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.37/dist/shoelace.js"></script>

<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.0/dist/barcodes/JsBarcode.ean-upc.min.js"></script>

<script src="/js/app.js"></script>
<link rel="stylesheet" type="text/css" href="/css/style.css">

Les deux premières lignes importent la bibliothèque de composants Web Shoelace (un script et une feuille de style).

La ligne suivante importe la bibliothèque JsBarcode pour créer les codes-barres des codes ISBN des livres.

Les dernières lignes importent notre propre code JavaScript et notre propre feuille de style CSS, situés dans nos sous-répertoires public/.

Dans le fichier body de la page HTML, nous utilisons les composants Lacet avec leurs balises d'éléments personnalisés, par exemple:

<sl-icon name="book-half"></sl-icon>
...

<sl-select id="language-select" placeholder="Select a language..." clearable>
    <sl-menu-item value="English">English</sl-menu-item>
    <sl-menu-item value="French">French</sl-menu-item>
    ...
</sl-select>
...

<sl-button id="more-button" type="primary" size="large">
    More books...
</sl-button>
...

Nous utilisons également des modèles HTML et leur capacité de remplissage de cases pour représenter un livre. Nous allons créer des copies de ce modèle pour remplir la liste des livres, et remplacer les valeurs des emplacements par les détails des livres:

    <template id="book-card">
        <sl-card class="card-overview">
        ...
            <slot name="author">Author</slot>
            ... 
        </sl-card>
    </template>

Il n'y a pas assez de code HTML et nous avons presque fini de revoir le code. Il reste un dernier élément essentiel: le code JavaScript côté client app.js qui interagit avec notre API REST.

Le code JavaScript app.js côté client

Commençons avec un écouteur d'événements de premier niveau qui attend que le contenu DOM soit chargé:

document.addEventListener("DOMContentLoaded", async function(event) {
    ...
}

Une fois que tout est prêt, nous pouvons configurer des constantes et des variables clés:

    const serverUrlResponse = await fetch('/webapi');
    const serverUrl = await serverUrlResponse.text();
    console.log('Web API endpoint:', serverUrl);
    
    const server = serverUrl + '/books';
    var page = 0;
    var language = '';

Nous allons d'abord récupérer l'URL de notre API REST, grâce au code de nœud App Engine qui renvoie la variable d'environnement que nous avons initialement définie dans app.yaml. Grâce à la variable d'environnement (le point de terminaison /webapi), appelé à partir du code JavaScript côté client, nous n'avons pas eu à coder en dur l'URL de l'API REST dans notre code de frontend.

Nous définissons également des variables page et language, que nous utiliserons pour suivre la pagination et le filtrage de langue.

    const moreButton = document.getElementById('more-button');
    moreButton.addEventListener('sl-focus', event => {
        console.log('Button clicked');
        moreButton.blur();

        appendMoreBooks(server, page++, language);
    });

Nous ajoutons un gestionnaire d'événements au bouton pour charger les livres. Lorsque l'utilisateur clique dessus, il appelle la fonction appendMoreBooks().

    const langSelect = document.getElementById('language-select');
    langSelect.addEventListener('sl-change', event => {
        page = 0;
        language = event.srcElement.value;
        document.getElementById('library').replaceChildren();
        console.log(`Language selected: "${language}"`);

        appendMoreBooks(server, page++, language);
    });

Comme pour la zone de sélection, nous ajoutons un gestionnaire d'événements pour être informé des modifications apportées à la langue sélectionnée. Comme pour le bouton, nous appelons également la fonction appendMoreBooks(), en transmettant l'URL de l'API REST, la page actuelle et la langue sélectionnée.

Voyons donc cette fonction qui récupère et ajoute des livres:

async function appendMoreBooks(server, page, language) {
    const searchUrl = new URL(server);
    if (!!page) searchUrl.searchParams.append('page', page);
    if (!!language) searchUrl.searchParams.append('language', language);
        
    const response = await fetch(searchUrl.href);
    const books = await response.json();
    ... 
}

Ci-dessus, nous créons l'URL exacte à utiliser pour appeler l'API REST. Nous pouvons normalement spécifier trois paramètres de requête, mais dans cette interface utilisateur, nous n'en spécifions que deux:

  • page : entier indiquant la page actuelle pour la pagination des livres.
  • language : chaîne de langue à filtrer par langue écrite.

Nous utilisons ensuite l'API Fetch pour récupérer le tableau JSON contenant les détails du livre.

    const linkHeader = response.headers.get('Link')
    console.log('Link', linkHeader);
    if (!!linkHeader && linkHeader.indexOf('rel="next"') > -1) {
        console.log('Show more button');
        document.getElementById('buttons').style.display = 'block';
    } else {
        console.log('Hide more button');
        document.getElementById('buttons').style.display = 'none';
    }

Selon que l'en-tête Link est présent ou non dans la réponse, nous afficherons ou masquerons le bouton [More books...], car l'en-tête Link nous indique s'il reste d'autres livres à charger (une URL next s'affiche dans l'en-tête Link).

    const library = document.getElementById('library');
    const template = document.getElementById('book-card');
    for (let book of books) {
        const bookCard = template.content.cloneNode(true);

        bookCard.querySelector('slot[name=title]').innerText = book.title;
        bookCard.querySelector('slot[name=language]').innerText = book.language;
        bookCard.querySelector('slot[name=author]').innerText = book.author;
        bookCard.querySelector('slot[name=year]').innerText = book.year;
        bookCard.querySelector('slot[name=pages]').innerText = book.pages;
        
        const img = document.createElement('img');
        img.setAttribute('id', book.isbn);
        img.setAttribute('class', 'img-barcode-' + book.isbn)
        bookCard.querySelector('slot[name=barcode]').appendChild(img);

        library.appendChild(bookCard);
        ... 
    }
}

Dans la section ci-dessus de la fonction, pour chaque livre renvoyé par l'API REST, nous allons cloner le modèle avec certains composants Web représentant un livre, et nous allons remplir les emplacements du modèle avec les détails du livre.

JsBarcode('.img-barcode-' + book.isbn).EAN13(book.isbn, {fontSize: 18, textMargin: 0, height: 60}).render();

Pour embellir le code ISBN, nous utilisons la bibliothèque JsBarcode afin de créer un bon code-barres, comme sur la quatrième de couverture de vrais livres.

Exécuter et tester l'application en local

Vous avez assez de code pour l'instant et il est temps de voir l'application en action. Nous allons d'abord le faire localement, dans Cloud Shell, avant de procéder au déploiement réel.

Nous installons les modules NPM nécessaires à notre application avec:

$ npm install

Et nous exécutons l'application comme d'habitude:

$ npm start

Ou avec l'actualisation automatique des modifications grâce à nodemon, avec:

$ npm run dev

L'application s'exécute localement et nous pouvons y accéder depuis le navigateur, à l'adresse http://localhost:8080.

Déployer l'application App Engine

Maintenant que nous avons la certitude que notre application s'exécute correctement en local, il est temps de la déployer sur App Engine.

Pour déployer l'application, lançons la commande suivante:

$ gcloud app deploy -q

L'application devrait être déployée au bout d'une minute environ.

L'application sera disponible à une URL au format suivant: https://${GOOGLE_CLOUD_PROJECT}.appspot.com.

Explorer l'interface utilisateur de notre application Web App Engine

Désormais, vous pouvez :

  • Cliquez sur le bouton [More books...] pour charger d'autres livres.
  • Sélectionnez une langue pour afficher uniquement les livres dans cette langue.
  • Pour revenir à la liste de tous les livres, vous pouvez effacer la sélection à l'aide de la petite croix affichée dans la zone de sélection.

10. Effectuer un nettoyage (facultatif)

Si vous n'avez pas l'intention de conserver l'application, vous pouvez nettoyer les ressources afin de réduire les coûts et d'utiliser le cloud de manière générale en supprimant l'intégralité du projet:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

11. Félicitations !

Nous avons créé un ensemble de services, grâce à Cloud Functions, App Engine et Cloud Run, afin d'exposer divers points de terminaison d'API Web et une interface Web, et de stocker, mettre à jour et parcourir une bibliothèque de livres, tout en respectant des modèles de conception adaptés au développement d'API REST.

Points abordés

  • Cloud Functions
  • Cloud Firestore
  • Cloud Run
  • App Engine

Aller plus loin

Si vous souhaitez approfondir cet exemple concret et le développer, voici une liste de choses que vous pourriez vouloir examiner:

  • Exploitez API Gateway pour fournir une façade d'API commune à la fonction d'importation de données et au conteneur d'API REST, ajouter des fonctionnalités telles que la gestion des clés API pour accéder à l'API, ou définir des limites de débit pour les utilisateurs d'API.
  • Déployez le module de nœud Swagger-UI dans l'application App Engine pour documenter et proposer un terrain de jeu de test pour l'API REST.
  • Dans l'interface, au-delà de la fonctionnalité de navigation existante, ajoutez des écrans supplémentaires pour modifier les données et créez des entrées de livre. De plus, comme nous utilisons la base de données Cloud Firestore, tirez parti de sa fonctionnalité en temps réel pour mettre à jour les données de livres affichées à mesure que des modifications sont apportées.