Atelier sur les API Web sans serveur

1. Aperçu

L'objectif de cet atelier de programmation est d'améliorer votre expérience avec les services "sans serveur" proposés par Google Cloud Platform:

  • Cloud Functions : 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, des éléments statiques, avec des fonctionnalités d'évolution rapide à la hausse ou à la baisse.
  • Cloud Run : pour déployer et faire évoluer des conteneurs pouvant contenir n'importe quel langage, environnement d'exécution ou bibliothèque.

Pour découvrir comment tirer parti de ces services sans serveur pour déployer et faire évoluer les API Web et REST, tout en suivant quelques 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 des livres disponibles dans notre bibliothèque, dans la base de données de documents Cloud Firestore, procédez comme suit :
  • Un conteneur Cloud Run qui exposera une API REST sur le contenu de notre base de données
  • Interface Web App Engine: pour parcourir la liste des livres, appelez notre API REST.

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

B6964f26b9624565.png

Points abordés

  • Cloud Functions
  • Cloud Firestore
  • Cloud Run
  • Service

2. Prérequis

Configuration de l'environnement au rythme de chacun

  1. Connectez-vous à Cloud Console, 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.

96a9c957bc475304.png

B9a10ebdf5b5a448.png

A1e3c01a38fa61c2.png

Mémorisez l'ID du projet. Il s'agit d'un nom unique permettant de différencier chaque projet Google Cloud (le nom ci-dessus est déjà pris ; vous devez en trouver un autre). Il sera désigné par le nom PROJECT_ID tout au long de cet atelier de programmation.

  1. Vous devez ensuite activer la facturation dans Cloud Console pour pouvoir utiliser les ressources Google Cloud.

L'exécution de cet atelier de programmation est très peu coûteuse, voire gratuite. Veillez à suivre les instructions de la section "Nettoyer" qui indique comment désactiver les ressources afin d'éviter les frais une fois ce tutoriel terminé. Les nouveaux utilisateurs de Google Cloud peuvent bénéficier d'un essai gratuit de 300 USD.

Démarrer Cloud Shell

Vous pouvez utiliser Google Cloud à distance depuis votre ordinateur portable. Dans cet atelier de programmation, vous allez utiliser Google Cloud Shell, un environnement de ligne de commande fonctionnant dans le cloud.

Depuis la console GCP, cliquez sur l'icône Cloud Shell de la barre d'outils située dans l'angle supérieur droit :

bce75f34b2c53987.png

Le provisionnement de l'environnement et la connexion ne devraient pas prendre plus de quelques minutes. Une fois l'opération terminée, le résultat devrait ressembler à ceci:

F6ef2b5f13479f3a.png

Cette machine virtuelle contient tous les outils de développement nécessaires. Il s'agit d'un répertoire d'accueil persistant de 5 Go qui s'exécute sur Google Cloud, ce qui améliore considérablement les performances et l'authentification du réseau. Vous pouvez travailler simplement dans un atelier à l'aide d'un simple navigateur.

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

Afin d'utiliser les différents services dont nous avons besoin dans le cadre de ce projet, nous allons activer quelques API. Pour ce faire, nous allons exécuter 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 quelques instants, l'opération devrait s'exécuter correctement:

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

Nous allons également configurer une variable d'environnement dont nous aurons besoin: la région cloud où déployer notre fonction, l'application et le conteneur:

$ export REGION=europe-west3

Comme nous allons stocker les 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 --region=${REGION}

Plus tard dans cet atelier de programmation, lorsque nous implémenterons l'API REST, nous devrons trier et filtrer les données. Pour cela, nous allons créer trois index:

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

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

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

Ces trois index correspondent aux recherches que nous effectuerons par auteur ou langue, tout en conservant l'ordre dans la collection via un champ mis à jour.

4. Obtenir le code

Obtenez 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 adaptée à 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 appropriés:

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

5. Exemples de données de la bibliothèque de livres

Le dossier des données contient un fichier books.json contenant une liste de cent livres, qui devrait probablement être lu. Ce document JSON est un tableau contenant des objets JSON. Examinons la forme des données qui seront ingérées 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 ce tableau contiennent les informations suivantes:

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

6. Point de terminaison de fonction pour importer des exemples de données de livres

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. Nous utiliserons Cloud Functions à cette fin.

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": "^1.7.1"
    },
    "scripts": {
        "start": "npx @google-cloud/functions-framework --target=parseBooks"
    }
}

Dans les dépendances d'exécution, nous n'avons besoin que du module @google-cloud/firestore de npm pour accéder à la base de données et stocker les données de nos livres. En coulisses, l'environnement d'exécution de 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 ordinateur (dans notre cas, dans Cloud Shell) pour exécuter des fonctions sans déployer chaque modification. Vous améliorez ainsi la boucle de rétroaction sur le développement.

Pour installer les dépendances, utilisez la commande install:

$ npm install

Le script start utilise le framework des fonctions pour vous donner une commande vous permettant d'exécuter la fonction en local à l'aide de l'instruction suivante:

$ npm start

Vous pouvez utiliser la commande curl ou potentiellement l'aperçu sur le Web de Cloud Shell pour interagir avec les fonctions GET de HTTP.

Passons maintenant au fichier index.js qui contient la logique de la 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 pointons sur la collection de livres (semblable à une table dans les bases de données relationnelles).

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

La fonction JavaScript parseBooks est en cours d'exportation. Il s'agit de la fonction que nous déclarerons lors du déploiement ultérieur.

Les deux instructions suivantes vérifient que:

  • Nous n'acceptons que les requêtes HTTP POST, et nous renvoyons un code d'état 405 pour indiquer que les autres méthodes HTTP ne sont pas autorisées.
  • Nous n'acceptons que des charges utiles application/json et 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 l'objet body de la requête. Nous préparons une opération Firestore par lot pour stocker tous les livres de façon groupée. Nous parlons du tableau JSON contenant les informations sur les livres, en examinant les champs isbn, title, author, language, pages et year. Le code ISBN du livre servira d'identifiant ou de clé primaire.

    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 vous envoyons un code d'état 400 pour vous en informer. Dans le cas contraire, 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:

$ npm install

Pour exécuter la fonction localement, grâce au framework des fonctions, nous allons utiliser la commande de script start 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 exécutez cette commande, la sortie suivante s'affiche, confirmant que la fonction s'exécute localement:

{"status":"OK"}

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

D6a2b31bfa3443f2.png

La capture d'écran ci-dessus montre la collection books créée, la liste des documents identifiés par le code ISBN du livre et les informations correspondantes 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 \
         --trigger-http \
         --runtime=nodejs12 \
         --allow-unauthenticated \
         --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 12. Nous déployons la fonction publiquement (idéalement, nous devons sécuriser ce point de terminaison). Nous devons spécifier la région dans laquelle réside la fonction. Nous faisons également pointer vers les sources du répertoire local et utilisons parseBooks (la fonction JavaScript exportée) comme point d'entrée.

Au bout de quelques minutes, la fonction est déployée dans le cloud. L'interface utilisateur de Cloud Console doit afficher la fonction:

C3156d50ba917ddd.png

Dans le résultat du déploiement, vous devriez voir l'URL de votre fonction, qui suit une certaine convention de dénomination (https://${REGION}-${GOOGLE_CLOUD_PROJECT}.cloudfunctions.net/${FUNCTION_NAME}), et bien sûr, cette URL de déclencheur HTTP est également disponible dans l'interface utilisateur de Cloud Console. onglet "déclencheur" :

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

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

Avec une commande curl similaire que nous avons utilisée précédemment pour tester la fonction exécutée en local, nous allons tester la fonction déployée. La seule modification sera l'URL:

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

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

{"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 en exposant cet ensemble de données.

7. Contrat d'API REST

Par exemple, même si vous ne définissez pas de contrat d'API à l'aide de la spécification OpenAPI, nous allons examiner les différents points de terminaison de notre API REST.

L'API peut échanger des objets JSON avec les informations suivantes:

  • isbn (facultatif) : String de 13 caractères représentant un code ISBN valide
  • author : String non vide représentant le nom de l'auteur du livre.
  • language : String non vide contenant la langue dans laquelle le livre a été écrit
  • pages : valeur Integer positive pour le nombre de pages du livre,
  • title : String (non vide) ayant pour titre le 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
  }

GET /books

Obtenez la liste de tous les livres, potentiellement filtrés par auteur et/ou langue, et paginés par périodes de 10 résultats à la fois.

Charge utile du corps: aucun

Paramètres de requête:

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

Affiche un tableau JSON d'objets de livre.

Codes d'état:

  • 200, lorsque la requête aboutit pour récupérer la liste des livres.
  • 400, en cas d'erreur.

/POST /books et POST /books/{isbn}

Publiez une nouvelle charge utile de livre, soit avec un paramètre de chemin isbn (dans ce cas, le code isbn n'est pas nécessaire dans la charge utile du livre), soit sans (dans ce cas, le code isbn doit être présent). charge utile du livre)

Charge utile du corps: objet de livre.

Paramètres de requête: aucun.

Renvoie: rien.

Codes d'état:

  • 201, une fois le livre stocké,
  • 406. Si le code isbn n'est pas valide,
  • 400, en cas d'erreur.

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

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

Affiche le livre mis à jour.

Codes d'état:

  • 200, une fois le livre 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 paramètre isbn transmis en tant que paramètre de chemin.

Charge utile du corps: aucun

Paramètres de requête: aucun.

Renvoie: rien.

Codes d'état:

  • 204, une fois le livre supprimé
  • 400, en cas d'erreur.

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

Explorer le code

Dockerfile

Commençons par Dockerfile, qui est chargé de conteneuriser le code de l'application:

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

Nous utilisons une image Node.JS 14 slim. Nous travaillons dans le répertoire /usr/src/app. Nous copions 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 cette application doit s'exécuter à 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": ">= 14.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écifiant que nous souhaitons utiliser Node.JS 14, comme c'était le cas avec Dockerfile.

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

  • Le module npm Firestore pour accéder aux données des livres dans 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 à partir du code client de l'interface de notre application Web App Engine :
  • Le framework Express, qui sera notre framework Web pour la conception de notre API,
  • Passons au module isbn3, qui permet de valider les codes ISBN de livres.

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

index.js

Passons à la viande du code et voyons 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érencez la collection books, dans laquelle sont stockées les données de nos livres.

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, notre 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 permet de manipuler les URL. Ce sera le cas lorsque nous créerons des en-têtes Link à des fins de pagination (nous y reviendrons plus en détail ultérieurement).

Nous configurerons ensuite le module cors. Nous avons explicite les en-têtes que nous souhaitons transmettre via CORS, car la plupart sont généralement supprimés, mais nous voulons conserver la longueur et le type de contenu habituels, ainsi que l'en-tête Link que nous allons spécifier 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 si la réponse est non valide.

  • GET /books

Jetons un œil au point de terminaison GET /books, petit à petit:

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 facultatifs de la requête pour permettre un filtrage par auteur et/ou par langue. Nous envoyons également la liste de livres par tranches de 10 livres.

En cas d'erreur au cours de la récupération des livres, une erreur 400 s'affiche.

Zoomons sur la partie coupée de ce point de terminaison:

        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é les données par author et language, mais dans cette section, nous allons trier la liste des livres en fonction de la date de dernière mise à jour (la plus récente vient en premier). Nous allons également paginer le résultat en définissant une limite (nombre d'éléments à afficher) et un décalage (point de départ à partir duquel renvoyer le lot de livres suivant).

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

Complétez les explications de ce point de terminaison en vous intéressant à une bonne pratique: en utilisant l'en-tête Link pour définir les liens URI vers la première, la page précédente ou la dernière page de données (dans le cas présent, nous fournissons uniquement les suivant).

        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 compliquée au début, mais nous allons ajouter un lien previous si nous ne sommes pas sur la première page de données. Et nous ajoutons un lien Suivant si la page de données est pleine (autrement dit, si elle contient le nombre maximal de livres défini par la constante PAGE_SIZE, en supposant qu'un autre soit associé à d'autres données). Nous utilisons ensuite la fonction resource#links() d'Express pour créer l'en-tête approprié avec la syntaxe correcte.

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

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

Les deux points de terminaison permettent de créer un livre. La première transmet le code ISBN dans la charge utile du livre, tandis que l'autre la transmet en tant que paramètre de chemin. Dans les deux cas, appelez la 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 renvoie la fonction (et définit un code d'état 406). Nous récupérons les champs de livre de la charge utile transmise dans le corps de la requête. Nous allons maintenant stocker les détails du livre dans Firestore. Retour de 201 en cas de réussite et 400 en cas d'échec.

Lors du renvoi réussi, nous définissons également l'en-tête d'emplacement pour fournir des repères au client de l'API où se trouve la nouvelle ressource. L'en-tête ressemblera à ceci:

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

Allons récupérer 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 permet de savoir si un livre a été trouvé. Dans le cas contraire, nous renvoyons une erreur et un code d'état 404 introuvable. Nous récupérons les données du livre et créons un objet JSON représentant le livre à renvoyer.

  • 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 modifions le champ de date et d'heure updated pour mémoriser la date 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 sont enregistrés, ce qui efface les champs existants de la mise à jour précédente ou de la création initiale).

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

  • DELETE /books/:isbn

Supprimer des livres est un jeu d'enfant. Nous appelons simplement la méthode delete() sur la référence du document. Nous affichons 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 par défaut sur le port 8080.

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:

$ npm install

Nous pouvons ensuite commencer par:

$ npm start

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

Il est également possible de créer un conteneur Docker et d'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 bon moyen de vérifier que la conteneurisation de notre application fonctionnera correctement lorsque nous la créerons 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 pouvons désormais exécuter quelques requêtes à son sujet.

  • 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éez un livre (ISBN dans un paramètre de chemin):
$ 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 à l'aide d'un ISBN:
$ curl http://localhost:8080/books/9780140449136
$ curl http://localhost:8080/books/9782070360536
  • Pour modifier un livre existant, modifiez simplement 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
  • Répertoriez les livres écrits en anglais:
$ curl http://localhost:8080/books?language=English
  • Chargez la quatrième page des livres:
$ curl http://localhost:8080/books?page=3

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

Développer et déployer l'API REST conteneurisée

Comme nous sommes ravis que l'API REST fonctionne conformément au plan, l'heure est venue de la déployer dans le cloud, sur Cloud Run.

Nous allons procéder par deux étapes:

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

À l'aide de 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 que l'interface utilisateur de Cloud Console apparaît désormais dans la liste:

4ca13b0a703b2139.png

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

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

Vous aurez besoin de l'URL de notre API REST Cloud Run dans la section suivante, car notre code frontend App Engine va interagir avec l'API.

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

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

Notre application, bien qu'elle soit déployée dans l'environnement d'exécution Node.JS App Engine, est principalement composée de ressources statiques. Le code de backend est faible, car la plupart des interactions de l'utilisateur sont effectuées dans le navigateur via JavaScript côté client. Nous n'utiliserons aucun framework JavaScript frontend. Nous allons simplement utiliser du code JavaScript "vanilla", avec quelques composants Web pour l'UI utilisant la bibliothèque de composants Web Shoelace:

  • une case pour sélectionner la langue du livre:

1b7bf64bd327b1ee.png

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

4jj54e4d5ee53367.png

  • et un bouton permettant de charger davantage de livres de la base de données:

4766c796a9d87475.png

Si vous combinez tous ces composants visuels, la page Web qui s'affiche dans notre bibliothèque se présentera comme suit:

fb6eae65811c8ac2.png

Le fichier de configuration app.yaml

Commençons par examiner 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, les différents handlers de l'application ou de spécifier que certaines ressources sont des éléments statiques, qui seront diffusé 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écifiant que notre application est un nœud Node.js et que nous voulons utiliser la version 14.

Ensuite, nous définissons une variable d'environnement qui pointe vers l'URL de notre service Cloud Run. Nous devrons mettre à jour l'espace réservé CHANGE_ME avec la bonne URL (voir ci-dessous comment modifier ce paramètre).

Ensuite, nous définissons différents gestionnaires. Les trois premières pointent vers l'emplacement du code HTML, CSS et JavaScript côté client, sous le dossier public/ et ses sous-dossiers. La quatrième indique que l'URL racine de notre application App Engine doit pointer vers la page index.html. De cette manière, le suffixe index.html ne s'affiche pas dans l'URL lorsque vous accédez à la racine du site Web. Et la dernière est l'URL par défaut qui achemine toutes les autres URL (/.*) vers notre application Node.js (par exemple, la partie dynamique de l'application, contrairement à l'URL statique). les éléments 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 qui pointe 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 aussi modifier manuellement la chaîne CHANGE_ME dans app.yaml avec l'URL appropriée:

env_variables:
    RUN_CRUD_SERVICE_URL: CHANGE_ME

Le fichier package.json de 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 tenons à souligner que nous souhaitons exécuter cette application à l'aide de Node.JS 14. Nous nous appuyons sur le framework Express, ainsi que sur le module npm isbn3 pour valider les codes ISBN des livres.

Dans les dépendances de développement, nous allons utiliser le module nodemon pour surveiller les modifications apportées aux fichiers. Même si nous pouvons exécuter notre application localement avec npm start, modifier le code, arrêter l'application avec ^C, puis la relancer, ce qui est assez fastidieux. Nous pouvons plutôt utiliser la commande suivante pour actualiser / redémarrer automatiquement l'application après un changement:

$ 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 spécifie que le répertoire public contient des éléments statiques qui peuvent être diffusés (au moins lors de l'exécution locale en mode développement) par le middleware static. Enfin, nous avons besoin de body-parser pour analyser nos charges utiles JSON.

Examinons les deux itinéraires que nous avons définis:

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 à / redirigera vers index.html dans notre répertoire public/html. Comme dans le mode développement, nous n'exécutons pas l'environnement d'exécution App Engine. Nous n'obtenons donc pas le routage des URL App Engine. À la place, nous allons simplement rediriger l'URL racine vers le fichier HTML.

Le deuxième point de terminaison que nous définissons /webapi affiche 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 écoute le port 8080 par défaut.

175index.html Page

Nous n'examinons pas toutes les lignes de cette longue page HTML. Mettez plutôt en évidence certaines 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, qui sont situés dans nos sous-répertoires public/.

Dans la section body de la page HTML, nous utilisons les composants Shoelace avec leurs balises d'élément personnalisées, 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 d'emplacements pour représenter un livre. Nous allons créer des copies de ce modèle pour renseigner la liste des livres et remplacer les valeurs par les espaces par les détails des livres:

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

La quantité de code HTML nécessaire est presque terminée. Dernière partie de viande: le code JavaScript côté client app.js qui interagit avec notre API REST.

Code JavaScript côté client app.js

Nous allons commencer par 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 le rapport est prêt, vous pouvez configurer des constantes et 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 = '';

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

Nous allons également définir des variables page et language, qui nous serviront à effectuer le suivi de la pagination et du filtrage linguistique.

    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 de chargement des livres. Lorsqu'un 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);
    });

De la même manière, pour le champ de sélection, nous ajoutons un gestionnaire d'événements afin d'être averti des modifications apportées à la sélection de langue. 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.

Examinons 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 nous n'en spécifiant que deux dans cette interface utilisateur:

  • page (nombre entier indiquant la page actuelle de 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 des livres.

    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 dans la réponse ou non, nous affichons ou masquons le bouton [More books...], car l'en-tête Link nous indique qu'il reste des livres à charger. être une URL next 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 des composants Web représentant un livre. Nous ajouterons les détails du livre dans les emplacements du modèle.

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

Pour simplifier un peu plus le code ISBN, nous utilisons la bibliothèque JsBarcode pour créer un bon code-barres, comme sur la quatrième de couverture des livres réels.

Exécuter et tester l'application en local

Il y a suffisamment de code pour l'instant. Il est temps de voir l'application en action. Dans un premier temps, nous le ferons localement, dans Cloud Shell, avant de procéder au déploiement réel.

Nous installons les modules npm requis par notre application avec:

$ npm install

Nous exécutez l'application comme d'habitude:

$ npm start

Ou, avec la recharge automatique des modifications via 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 sommes certains que notre application fonctionne correctement localement, 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 après une minute.

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

Explorer l'UI de notre application Web App Engine

Voici ce que vous pouvez faire désormais :

  • Cliquez sur le bouton [More books...] pour charger d'autres livres.
  • Sélectionnez une langue pour afficher les livres uniquement dans cette langue.
  • Vous pouvez effacer la sélection en cliquant sur la petite croix dans la zone de sélection pour revenir à la liste de tous les livres.

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'être un bon administreur du cloud en supprimant tout le projet:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT}

11. Félicitations !

Grâce à Cloud Functions, App Engine et Cloud Run, nous avons créé un ensemble de services permettant d'exposer différents points de terminaison Web et interface Web pour stocker, mettre à jour et parcourir une bibliothèque de livres, en suivant des modèles de conception efficaces pour le développement d'API REST.

Points abordés

  • Cloud Functions
  • Cloud Firestore
  • Cloud Run
  • Service

Pour aller plus loin

Si vous voulez en savoir plus sur cet exemple concret et l'étendre, voici une liste d'éléments à examiner:

  • Avec API Gateway, définissez une façade d'API commune à la fonction d'importation de données et au conteneur d'API REST afin d'ajouter des fonctionnalités telles que la gestion des clés API pour accéder à l'API, ou une limitation du 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 une aire de test pour l'API REST.
  • Sur l'interface, au-delà de la fonctionnalité de navigation existante, ajoutez des écrans supplémentaires pour modifier les données et créer des entrées de livre. De plus, comme nous utilisons la base de données Cloud Firestore, profitez de sa fonctionnalité en temps réel pour mettre à jour les données affichées dans les livres à mesure que des modifications sont apportées.