Workshop sulle API web serverless

1. Panoramica

Lo scopo di questo codelab è acquisire esperienza con i servizi "serverless" offerti da Google Cloud Platform:

  • Cloud Functions: per eseguire il deployment di piccole unità di logica di business sotto forma di funzioni che reagiscono a vari eventi (messaggi Pub/Sub, nuovi file in Cloud Storage, richieste HTTP e altro ancora).
  • App Engine: per eseguire il deployment e pubblicare app web, API web, backend mobile e asset statici, con funzionalità di scalabilità rapida, inclusa la possibilità di fare lo scale up e lo scale down rapidamente,
  • Cloud Run: per eseguire il deployment e scalare i container, che possono contenere qualsiasi linguaggio, runtime o libreria.

E per scoprire come sfruttare questi servizi serverless per eseguire il deployment e scalare le API web e REST, oltre a vedere alcuni buoni principi di progettazione RESTful.

In questo workshop creeremo un esploratore di librerie composto da:

  • Una funzione Cloud Functions: per importare il set di dati iniziale dei libri disponibili nella nostra biblioteca nel database di documenti Cloud Firestore.
  • Un container Cloud Run: che esporrà un'API REST sui contenuti del nostro database.
  • Un frontend web di App Engine: per sfogliare l'elenco dei libri, chiamando la nostra API REST.

Ecco l'aspetto del frontend web alla fine di questo codelab:

705e014da0ca5e90.png

Cosa imparerai a fare

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

2. Configurazione e requisiti

Configurazione dell'ambiente autonomo

  1. Accedi alla console Google Cloud e crea un nuovo progetto o riutilizzane uno esistente. Se non hai ancora un account Gmail o Google Workspace, devi crearne uno.

295004821bab6a87.png

37d264871000675d.png

96d86d3d5655cdbe.png

  • Il nome del progetto è il nome visualizzato per i partecipanti a questo progetto. È una stringa di caratteri non utilizzata dalle API di Google. Puoi sempre aggiornarlo.
  • L'ID progetto è univoco in tutti i progetti Google Cloud ed è immutabile (non può essere modificato dopo l'impostazione). La console Cloud genera automaticamente una stringa univoca, di solito non ti interessa di cosa si tratta. Nella maggior parte dei codelab, dovrai fare riferimento all'ID progetto (in genere identificato come PROJECT_ID). Se l'ID generato non ti piace, puoi generarne un altro casuale. In alternativa, puoi provare a crearne uno e vedere se è disponibile. Non può essere modificato dopo questo passaggio e rimane per tutta la durata del progetto.
  • Per tua informazione, esiste un terzo valore, un numero di progetto, utilizzato da alcune API. Scopri di più su tutti e tre questi valori nella documentazione.
  1. Successivamente, devi abilitare la fatturazione in Cloud Console per utilizzare le risorse/API Cloud. Completare questo codelab non costa molto, se non nulla. Per arrestare le risorse ed evitare addebiti oltre a quelli previsti in questo tutorial, puoi eliminare le risorse che hai creato o il progetto. I nuovi utenti di Google Cloud possono beneficiare del programma prova senza costi di 300$.

Avvia Cloud Shell

Sebbene Google Cloud possa essere gestito da remoto dal tuo laptop, in questo codelab utilizzerai Google Cloud Shell, un ambiente a riga di comando in esecuzione nel cloud.

Nella console Google Cloud, fai clic sull'icona di Cloud Shell nella barra degli strumenti in alto a destra:

84688aa223b1c3a2.png

Bastano pochi istanti per eseguire il provisioning e connettersi all'ambiente. Al termine, dovresti vedere un risultato simile a questo:

320e18fedb7fbe0.png

Questa macchina virtuale è caricata con tutti gli strumenti per sviluppatori di cui avrai bisogno. Offre una home directory permanente da 5 GB e viene eseguita su Google Cloud, migliorando notevolmente le prestazioni e l'autenticazione della rete. Tutto il lavoro in questo codelab può essere svolto all'interno di un browser. Non devi installare nulla.

3. Prepara l'ambiente e abilita le API cloud

Per utilizzare i vari servizi di cui avremo bisogno durante questo progetto, abiliteremo alcune API. A questo scopo, eseguiamo questo comando in Cloud Shell:

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

Dopo un po' di tempo, l'operazione dovrebbe essere completata correttamente:

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

Configuriamo anche una variabile di ambiente che ci servirà durante il percorso: la regione cloud in cui eseguiremo il deployment della funzione, dell'app e del container:

$ export REGION=europe-west3

Poiché archivieremo i dati nel database Cloud Firestore, dovremo creare il database:

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

Più avanti in questo codelab, quando implementeremo l'API REST, dovremo ordinare e filtrare i dati. A questo scopo, creeremo tre indici:

$ 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 

Questi tre indici corrispondono alle ricerche che eseguiremo per autore o lingua, mantenendo l'ordine nella raccolta tramite un campo aggiornato.

4. Ottieni il codice

Recupera il codice dal seguente repository GitHub:

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

Il codice dell'applicazione è scritto utilizzando Node.JS.

Avrai la seguente struttura di cartelle pertinente per questo lab:

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

Queste sono le cartelle pertinenti:

  • data: questa cartella contiene un campione di dati di un elenco di 100 libri.
  • function-import: questa funzione offre un endpoint per importare dati di esempio.
  • run-crud: questo container esporrà un'API web per accedere ai dati dei libri archiviati in Cloud Firestore.
  • appengine-frontend: questa applicazione web App Engine mostrerà un semplice frontend di sola lettura per sfogliare l'elenco dei libri.

5. Dati della raccolta di libri di esempio

Nella cartella dei dati, abbiamo un file books.json che contiene un elenco di cento libri, probabilmente da leggere. Questo documento JSON è un array contenente oggetti JSON. Diamo un'occhiata alla forma dei dati che inseriremo tramite una funzione Cloud Functions:

[
  {
    "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
  },
  ...
]

Tutte le nostre voci di libri in questo array contengono le seguenti informazioni:

  • isbn: il codice ISBN-13 che identifica il libro.
  • author: il nome dell'autore del libro.
  • language: la lingua parlata in cui è scritto il libro.
  • pages: il numero di pagine del libro.
  • title: il titolo del libro.
  • year: l'anno di pubblicazione del libro.

6. Un endpoint di funzione per importare dati di esempio dei libri

In questa prima sezione, implementeremo l'endpoint che verrà utilizzato per importare i dati dei libri di esempio. A questo scopo, utilizzeremo Cloud Functions.

Esplora il codice

Iniziamo esaminando il file 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"
    }
}

Nelle dipendenze di runtime, abbiamo bisogno solo del modulo NPM @google-cloud/firestore per accedere al database e archiviare i dati dei libri. Sotto il cofano, il runtime di Cloud Functions fornisce anche il framework web Express, quindi non è necessario dichiararlo come dipendenza.

Nelle dipendenze di sviluppo, dichiariamo il framework di Functions (@google-cloud/functions-framework), che è il framework di runtime utilizzato per richiamare le funzioni. Si tratta di un framework open source che puoi utilizzare anche localmente sul tuo computer (nel nostro caso, all'interno di Cloud Shell) per eseguire le funzioni senza eseguire il deployment ogni volta che apporti una modifica, migliorando così il ciclo di feedback di sviluppo.

Per installare le dipendenze, utilizza il comando install:

$ npm install

Lo script start utilizza Functions Framework per fornirti un comando che puoi utilizzare per eseguire la funzione localmente con la seguente istruzione:

$ npm start

Puoi utilizzare curl o potenzialmente l'anteprima web di Cloud Shell per le richieste HTTP GET per interagire con la funzione.

Ora diamo un'occhiata al file index.js che contiene la logica della nostra funzione di importazione dei dati dei libri:

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

Creiamo un'istanza del modulo Firestore e puntiamo alla raccolta dei libri (simile a una tabella nei database relazionali).

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

Stiamo esportando la funzione JavaScript parseBooks. Questa è la funzione che dichiareremo quando la implementeremo in un secondo momento.

Le prossime istruzioni verificano che:

  • Accettiamo solo richieste HTTP POST e restituiamo un codice di stato 405 per indicare che gli altri metodi HTTP non sono consentiti.
  • Accettiamo solo payload application/json e inviamo un codice di stato 406 per indicare che questo formato di payload non è accettabile.
    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()
        });
    }

Quindi, possiamo recuperare il payload JSON tramite body della richiesta. Stiamo preparando un'operazione batch di Firestore per archiviare tutti i libri collettivamente. Iteriamo l'array JSON costituito dai dettagli del libro, esaminando i campi isbn, title, author, language, pages e year. Il codice ISBN del libro fungerà da chiave primaria o identificatore.

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

Ora che la maggior parte dei dati è pronta, possiamo eseguire il commit dell'operazione. Se l'operazione di archiviazione non va a buon fine, restituiamo un codice di stato 400 per indicare che non è riuscita. In caso contrario, possiamo restituire una risposta OK, con un codice di stato 202 che indica che la richiesta di salvataggio collettivo è stata accettata.

Esecuzione e test della funzione di importazione

Prima di eseguire il codice, installiamo le dipendenze con:

$ npm install

Per eseguire la funzione in locale, grazie al framework di Functions, utilizzeremo il comando dello script start che abbiamo definito in package.json:

$ npm start

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

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

Per inviare una richiesta HTTP POST alla tua funzione locale, puoi eseguire:

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

Quando esegui questo comando, visualizzerai il seguente output, che conferma che la funzione è in esecuzione in locale:

{"status":"OK"}

Puoi anche andare all'interfaccia utente di Cloud Console per verificare che i dati siano effettivamente archiviati in Firestore:

409982568cebdbf8.png

Nello screenshot precedente, possiamo vedere la raccolta books creata, l'elenco dei documenti del libro identificati dal codice ISBN del libro e i dettagli di quella particolare voce del libro a destra.

Deployment della funzione nel cloud

Per eseguire il deployment della funzione in Cloud Functions, utilizzeremo il seguente comando nella directory function-import:

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

Eseguiamo il deployment della funzione con il nome simbolico bulk-import. Questa funzione viene attivata tramite richieste HTTP. Utilizziamo il runtime Node.js 20. Eseguiamo il deployment della funzione pubblicamente (idealmente, dovremmo proteggere questo endpoint). Specifichiamo la regione in cui vogliamo che risieda la funzione. Indichiamo le origini nella directory locale e utilizziamo parseBooks (la funzione JavaScript esportata) come punto di ingresso.

Dopo un paio di minuti o meno, la funzione viene implementata nel cloud. Nell'interfaccia utente della console Cloud, dovresti vedere la funzione:

c910875d4dc0aaa8.png

Nell'output del deployment, dovresti essere in grado di visualizzare l'URL della funzione, che segue una determinata convenzione di denominazione (https://${REGION}-${GOOGLE_CLOUD_PROJECT}.cloudfunctions.net/${FUNCTION_NAME}). Naturalmente, puoi trovare questo URL del trigger HTTP anche nell'interfaccia utente di Cloud Console, nella scheda Trigger:

380ffc46eb56441e.png

Puoi anche recuperare l'URL tramite la riga di comando con gcloud:

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

Memorizziamolo nella variabile di ambiente BULK_IMPORT_URL, in modo da poterlo riutilizzare per testare la funzione di cui è stato eseguito il deployment.

Testare la funzione di cui è stato eseguito il deployment

Con un comando curl simile a quello utilizzato in precedenza per testare la funzione in esecuzione localmente, testeremo la funzione di cui è stato eseguito il deployment. L'unica modifica sarà l'URL:

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

Anche in questo caso, se l'operazione ha esito positivo, dovrebbe restituire il seguente output:

{"status":"OK"}

Ora che la nostra funzione di importazione è implementata e pronta e che abbiamo caricato i nostri dati di esempio, è il momento di sviluppare l'API REST che espone questo set di dati.

7. Il contratto API REST

Sebbene non definiamo un contratto API utilizzando, ad esempio, la specifica OpenAPI, esamineremo i vari endpoint della nostra API REST.

L'API scambia oggetti JSON di libri, costituiti da:

  • isbn (facoltativo): un String di 13 caratteri che rappresenta un codice ISBN valido.
  • author: un String non vuoto che rappresenta il nome dell'autore del libro.
  • language: un String non vuoto contenente la lingua in cui è stato scritto il libro.
  • pages: un Integer positivo per il numero di pagine del libro.
  • title: un String non vuoto con il titolo del libro.
  • year: un valore Integer per l'anno di pubblicazione del libro.

Esempio di payload del libro:

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

GET /books

Ottieni l'elenco di tutti i libri, potenzialmente filtrati per autore e/o lingua e paginati per finestre di 10 risultati alla volta.

Payload del corpo: nessuno.

Parametri di query:

  • author (facoltativo): filtra l'elenco libri per autore,
  • language (facoltativo) - filtra l'elenco libri per lingua,
  • page (facoltativo, valore predefinito = 0) indica il ranking della pagina dei risultati da restituire.

Restituisce un array JSON di oggetti libro.

Codici di stato:

  • 200: quando la richiesta riesce a recuperare l'elenco dei libri,
  • 400, se si verifica un errore.

POST /books e POST /books/{isbn}

Pubblica un nuovo payload del libro, con un parametro di percorso isbn (nel qual caso il codice isbn non è necessario nel payload del libro) o senza (nel qual caso il codice isbn deve essere presente nel payload del libro)

Payload del corpo: un oggetto libro.

Parametri di query: nessuno.

Resi: niente.

Codici di stato:

  • 201: quando il libro viene archiviato correttamente,
  • 406: se il codice isbn non è valido,
  • 400, se si verifica un errore.

GET /books/{isbn}

Recupera un libro dalla raccolta, identificato dal relativo codice isbn, passato come parametro di percorso.

Payload del corpo: nessuno.

Parametri di query: nessuno.

Restituisce: un oggetto JSON libro o un oggetto errore se il libro non esiste.

Codici di stato:

  • 200: se il libro viene trovato nel database,
  • 400: se si verifica un errore,
  • 404: se non è stato possibile trovare il libro,
  • 406: se il codice isbn non è valido.

PUT /books/{isbn}

Aggiorna un libro esistente, identificato dal relativo isbn passato come parametro di percorso.

Payload del corpo: un oggetto libro. Possono essere passati solo i campi che richiedono un aggiornamento, gli altri sono facoltativi.

Parametri di query: nessuno.

Restituisce: il libro aggiornato.

Codici di stato:

  • 200: quando il libro viene aggiornato correttamente,
  • 400: se si verifica un errore,
  • 406: se il codice isbn non è valido.

DELETE /books/{isbn}

Elimina un libro esistente, identificato dal relativo isbn passato come parametro di percorso.

Payload del corpo: nessuno.

Parametri di query: nessuno.

Resi: niente.

Codici di stato:

  • 204: quando il libro viene eliminato correttamente,
  • 400, se si verifica un errore.

8. Esegui il deployment ed esponi un'API REST in un container

Esplora il codice

Dockerfile

Iniziamo esaminando il Dockerfile, che sarà responsabile del containerizzazione del codice dell'applicazione:

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

Utilizziamo un'immagine "slim" di Node.JS 20. Stiamo lavorando nella directory /usr/src/app. Stiamo copiando il file package.json (dettagli di seguito) che definisce, tra le altre cose, le nostre dipendenze. Installiamo le dipendenze con npm install, copiando il codice sorgente. Infine, indichiamo come deve essere eseguita questa applicazione con il comando node index.js.

package.json

Successivamente, possiamo dare un'occhiata al file 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"
    }
}

Specifichiamo che vogliamo utilizzare Node.js 14, come nel caso di Dockerfile.

La nostra applicazione API web dipende da:

  • Il modulo NPM Firestore per accedere ai dati dei libri nel database.
  • La libreria cors per gestire le richieste di CORS, poiché la nostra API REST verrà richiamata dal codice client del frontend della nostra applicazione web App Engine.
  • Il framework Express, che sarà il nostro framework web per la progettazione della nostra API,
  • e poi il modulo isbn3, che aiuta a convalidare i codici ISBN dei libri.

Specifichiamo anche lo script start, che sarà utile per avviare l'applicazione localmente, a scopo di sviluppo e test.

index.js

Passiamo ora al cuore del codice, con un'analisi approfondita di index.js:

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

Richiediamo il modulo Firestore e facciamo riferimento alla raccolta books, in cui sono archiviati i dati dei nostri libri.

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

Utilizziamo Express come framework web per implementare la nostra API REST. Utilizziamo il modulo body-parser per analizzare i payload JSON scambiati con la nostra API.

Il modulo querystring è utile per manipolare gli URL. Questo sarà il caso quando creeremo le intestazioni Link per scopi di impaginazione (maggiori dettagli in seguito).

Poi configuriamo il modulo cors. Specifichiamo le intestazioni che vogliamo vengano trasmesse tramite CORS, poiché la maggior parte viene solitamente rimossa, ma in questo caso vogliamo mantenere la lunghezza e il tipo di contenuti abituali, nonché l'intestazione Link che specificheremo per la paginazione.

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

Utilizzeremo il modulo NPM isbn3 per analizzare e convalidare i codici ISBN e svilupperemo una piccola funzione di utilità che analizzerà i codici ISBN e risponderà con un codice di stato 406 nella risposta, se i codici ISBN non sono validi.

  • GET /books

Diamo un'occhiata all'endpoint GET /books, parte per parte:

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

Ci stiamo preparando a eseguire una query sul database preparando una query. Questa query dipenderà dai parametri di query facoltativi, per filtrare per autore e/o per lingua. Restituiamo anche l'elenco libri in blocchi di 10 libri.

Se si verifica un errore durante il recupero dei libri, restituiamo un errore con un codice di stato 400.

Esaminiamo la parte tagliata dell'endpoint:

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

Nella sezione precedente abbiamo filtrato in base a author e language, ma in questa sezione ordineremo l'elenco dei libri in base alla data dell'ultimo aggiornamento (l'ultimo aggiornamento viene visualizzato per primo). Inoltre, pagineremo il risultato definendo un limite (il numero di elementi da restituire) e un offset (il punto di partenza da cui restituire il batch successivo di libri).

Eseguiamo la query, otteniamo lo snapshot dei dati e inseriamo i risultati in un array JavaScript che verrà restituito alla fine della funzione.

Completiamo le spiegazioni di questo endpoint esaminando una best practice: l'utilizzo dell'intestazione Link per definire i link URI alla prima, alla precedente, alla successiva o all'ultima pagina di dati (nel nostro caso, forniremo solo la precedente e la successiva).

        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 logica potrebbe sembrare un po' complessa all'inizio, ma quello che stiamo facendo è aggiungere un link Precedente se non ci troviamo nella prima pagina di dati. Aggiungiamo un link next se la pagina di dati è piena (ovvero contiene il numero massimo di libri definito dalla costante PAGE_SIZE, supponendo che ne arrivi un'altra con altri dati). Utilizziamo quindi la funzione resource#links() di Express per creare l'intestazione corretta con la sintassi giusta.

Per tua informazione, l'intestazione del link sarà simile a questa:

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

Entrambi gli endpoint servono per creare un nuovo libro. Uno trasmette il codice ISBN nel payload del libro, mentre l'altro lo trasmette come parametro di percorso. In entrambi i casi, viene chiamata la funzione 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}`});
    }    
}

Controlliamo che il codice isbn sia valido, altrimenti la funzione restituisce un valore (e imposta un codice di stato 406). Recuperiamo i campi del libro dal payload passato nel corpo della richiesta. Quindi, archivieremo i dettagli del libro in Firestore. Restituisce 201 in caso di esito positivo e 400 in caso di esito negativo.

In caso di restituzione riuscita, impostiamo anche l'intestazione Location, in modo da fornire al client dell'API indicazioni sulla posizione della risorsa appena creata. L'intestazione avrà il seguente aspetto:

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

Recuperiamo un libro, identificato tramite il suo codice ISBN, da 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}`});
    }
});

Come sempre, verifichiamo la validità del codice ISBN. Eseguiamo una query su Firestore per recuperare il libro. La proprietà snapshot.exists è utile per sapere se è stato trovato un libro. In caso contrario, restituiamo un errore e un codice di stato 404 Non trovato. Recuperiamo i dati del libro e creiamo un oggetto JSON che lo rappresenta, da restituire.

  • PUT /books/:isbn

Stiamo utilizzando il metodo PUT per aggiornare un libro esistente.

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

Aggiorniamo il campo data/ora updated per ricordare l'ultima volta che abbiamo aggiornato questo record. Utilizziamo la strategia {merge:true}, che sostituisce i campi esistenti con i nuovi valori (altrimenti, tutti i campi vengono rimossi e vengono salvati solo i nuovi campi nel payload, cancellando i campi esistenti dall'aggiornamento precedente o dalla creazione iniziale).

Inoltre, impostiamo l'intestazione Location in modo che punti all'URI del libro.

  • DELETE /books/:isbn

L'eliminazione dei libri è piuttosto semplice. Chiamiamo semplicemente il metodo delete() sul riferimento al documento. Restituiamo un codice di stato 204, in quanto non restituiamo alcun contenuto.

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

Avvia il server Express / Node

Infine, avviamo il server, in ascolto sulla porta 8080 per impostazione predefinita:

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

Esecuzione dell'applicazione in locale

Per eseguire l'applicazione localmente, installiamo prima le dipendenze con:

$ npm install

A questo punto possiamo iniziare con:

$ npm start

Il server verrà avviato su localhost e rimarrà in ascolto sulla porta 8080 per impostazione predefinita.

È anche possibile creare un container Docker ed eseguire l'immagine container con i seguenti comandi:

$ docker build -t crud-web-api .

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

L'esecuzione all'interno di Docker è anche un ottimo modo per verificare che la containerizzazione della nostra applicazione funzioni correttamente durante la creazione nel cloud con Cloud Build.

Test dell'API

Indipendentemente da come eseguiamo il codice dell'API REST (direttamente tramite Node o tramite un'immagine container Docker), ora possiamo eseguire alcune query.

  • Crea un nuovo libro (ISBN nel payload del corpo):
$ curl -XPOST -d '{"isbn":"9782070368228","title":"Book","author":"me","pages":123,"year":2021,"language":"French"}' \
    -H "Content-Type: application/json" \
    http://localhost:8080/books
  • Crea un nuovo libro (ISBN in un parametro di percorso):
$ curl -XPOST -d '{"title":"Book","author":"me","pages":123,"year":2021,"language":"French"}' \
    -H "Content-Type: application/json" \
    http://localhost:8080/books/9782070368228
  • Elimina un libro (quello che abbiamo creato):
$ curl -XDELETE http://localhost:8080/books/9782070368228
  • Recuperare un libro tramite ISBN:
$ curl http://localhost:8080/books/9780140449136
$ curl http://localhost:8080/books/9782070360536
  • Aggiorna un libro esistente modificando solo il titolo:
$ curl -XPUT \
       -d '{"title":"Book"}' \
       -H "Content-Type: application/json" \      
       http://localhost:8080/books/9780003701203
  • Recupera l'elenco dei libri (i primi 10):
$ curl http://localhost:8080/books
  • Trova i libri scritti da un determinato autore:
$ curl http://localhost:8080/books?author=Virginia+Woolf
  • Elenca i libri scritti in inglese:
$ curl http://localhost:8080/books?language=English
  • Carica la quarta pagina di libri:
$ curl http://localhost:8080/books?page=3

Possiamo anche combinare i parametri di query author, language e books per perfezionare la ricerca.

Creazione e deployment dell'API REST containerizzata

Siamo felici che l'API REST funzioni come previsto, quindi è il momento giusto per eseguirne il deployment nel cloud, su Cloud Run.

Lo faremo in due passaggi:

  • Innanzitutto, crea l'immagine container con Cloud Build utilizzando il seguente comando:
$ gcloud builds submit \
         --tag gcr.io/${GOOGLE_CLOUD_PROJECT}/crud-web-api
  • Quindi, eseguendo il deployment del servizio con questo secondo comando:
$ gcloud run deploy run-crud \
         --image gcr.io/${GOOGLE_CLOUD_PROJECT}/crud-web-api \
         --allow-unauthenticated \
         --region=${REGION} \
         --platform=managed

Con il primo comando, Cloud Build crea l'immagine container e la ospita in Container Registry. Il comando successivo esegue il deployment dell'immagine container dal registro e nella regione cloud.

Possiamo verificare nell'interfaccia utente della console Cloud che il nostro servizio Cloud Run ora venga visualizzato nell'elenco:

f62fbca02a8127c0.png

L'ultimo passaggio che eseguiremo qui è recuperare l'URL del servizio Cloud Run appena implementato, grazie al seguente comando:

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

Nella sezione successiva avremo bisogno dell'URL della nostra API REST di Cloud Run, poiché il codice frontend di App Engine interagirà con l'API.

9. Ospita un'app web per sfogliare la raccolta

L'ultimo pezzo del puzzle per aggiungere un tocco di brillantezza a questo progetto è fornire un frontend web che interagisca con la nostra API REST. A questo scopo, utilizzeremo Google App Engine, con un codice JavaScript client che chiamerà l'API tramite richieste AJAX (utilizzando l'API Fetch lato client).

La nostra applicazione, sebbene sia stata eseguita il deployment nel runtime Node.JS App Engine, è costituita principalmente da risorse statiche. Non è presente molto codice di backend, poiché la maggior parte dell'interazione utente avverrà nel browser tramite JavaScript lato client. Non utilizzeremo alcun framework JavaScript frontend sofisticato, ma solo un po' di JavaScript "puro", con alcuni componenti web per la UI che utilizzano la libreria di componenti web Shoelace:

  • una casella di selezione per scegliere la lingua del libro:

6fb9f741000a2dc1.png

  • un componente della scheda per visualizzare i dettagli di un determinato libro (incluso un codice a barre per rappresentare l'ISBN del libro, utilizzando la libreria JsBarcode):

3aa21a9e16e3244e.png

  • e un pulsante per caricare altri libri dal database:

3925ad81c91bbac9.png

Quando combini tutti questi componenti visivi, la pagina web risultante per sfogliare la nostra raccolta sarà simile alla seguente:

18a5117150977d6.png

Il app.yaml file di configurazione

Iniziamo a esaminare la codebase di questa applicazione App Engine esaminando il file di configurazione app.yaml. Si tratta di un file specifico di App Engine che consente di configurare elementi come le variabili di ambiente, i vari "gestori" dell'applicazione o di specificare che alcune risorse sono asset statici, che verranno pubblicati dalla CDN integrata di 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

Specifichiamo che la nostra applicazione è Node.JS e che vogliamo utilizzare la versione 14.

Poi definiamo una variabile di ambiente che punta all'URL del nostro servizio Cloud Run. Dovremo aggiornare il segnaposto CHANGE_ME con l'URL corretto (vedi di seguito come modificarlo).

Dopodiché, definiamo vari gestori. I primi tre puntano alla posizione del codice lato client HTML, CSS e JavaScript, nella cartella public/ e nelle relative sottocartelle. Il quarto indica che l'URL principale della nostra applicazione App Engine deve puntare alla pagina index.html. In questo modo, non vedremo il suffisso index.html nell'URL quando accediamo alla radice del sito web. L'ultimo è quello predefinito che indirizzerà tutti gli altri URL (/.*) alla nostra applicazione Node.JS (ovvero la parte "dinamica" dell'applicazione, in contrasto con gli asset statici che abbiamo descritto).

Ora aggiorniamo l'URL dell'API web del servizio Cloud Run.

Nella directory appengine-frontend/, esegui questo comando per aggiornare la variabile di ambiente che punta all'URL della nostra API REST basata su Cloud Run:

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

In alternativa, modifica manualmente la stringa CHANGE_ME in app.yaml con l'URL corretto:

env_variables:
    RUN_CRUD_SERVICE_URL: CHANGE_ME

Il file 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"
    }
}

Ribadiamo che vogliamo eseguire questa applicazione utilizzando Node.JS 14. Ci basiamo sul framework Express e sul modulo isbn3 NPM per la convalida dei codici ISBN dei libri.

Nelle dipendenze di sviluppo, utilizzeremo il modulo nodemon per monitorare le modifiche ai file. Anche se possiamo eseguire la nostra applicazione localmente con npm start, apportare alcune modifiche al codice, arrestare l'app con ^C e poi riavviarla, è un po' noioso. In alternativa, possiamo utilizzare il seguente comando per ricaricare / riavviare automaticamente l'applicazione in caso di modifiche:

$ npm run dev

Il index.js codice Node.JS

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

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

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

È necessario il framework web Express. Specifichiamo che la directory pubblica contiene asset statici che possono essere pubblicati (almeno quando vengono eseguiti localmente in modalità di sviluppo) dal middleware static. Infine, richiediamo body-parser per analizzare i nostri payload JSON.

Diamo un'occhiata alle due route che abbiamo definito:

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

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

Il primo risultato corrispondente a / reindirizzerà a index.html nella nostra directory public/html. Poiché in modalità di sviluppo non viene eseguito l'ambiente di runtime di App Engine, non viene eseguito il routing degli URL di App Engine. Quindi, qui reindirizziamo semplicemente l'URL principale al file HTML.

Il secondo endpoint che definiamo /webapi restituirà l'URL della nostra API REST Cloud Run. In questo modo, il codice JavaScript lato client saprà dove chiamare per ottenere l'elenco dei libri.

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

Per finire, eseguiamo l'app web Express e ascoltiamo sulla porta 8080 per impostazione predefinita.

La pagina index.html

Non esamineremo ogni riga di questa lunga pagina HTML. Concentriamoci invece su alcune righe chiave.

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

Le prime due righe importano la libreria di componenti web Shoelace (uno script e un foglio di stile).

La riga successiva importa la libreria JsBarcode per creare i codici a barre dei codici ISBN del libro.

Le ultime righe importano il nostro codice JavaScript e il nostro foglio di stile CSS, che si trovano nelle nostre sottodirectory public/.

Nella sezione body della pagina HTML, utilizziamo i componenti Shoelace con i relativi tag di elementi personalizzati, ad esempio:

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

Inoltre, utilizziamo i modelli HTML e la loro funzionalità di riempimento degli slot per rappresentare un libro. Creeremo copie di questo modello per compilare l'elenco dei libri e sostituiremo i valori negli spazi con i dettagli dei libri:

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

Basta HTML, abbiamo quasi finito di esaminare il codice. Manca un'ultima parte sostanziale: il codice JavaScript lato client di app.js che interagisce con la nostra API REST.

Il codice JavaScript lato client app.js

Iniziamo con un listener di eventi di primo livello che attende il caricamento dei contenuti DOM:

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

Una volta pronto, possiamo impostare alcune costanti e variabili chiave:

    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 = '';

Innanzitutto, recupereremo l'URL della nostra API REST, grazie al codice del nodo App Engine che restituisce la variabile di ambiente che abbiamo impostato inizialmente in app.yaml. Grazie alla variabile di ambiente, l'endpoint /webapi, chiamato dal codice JavaScript lato client, non abbiamo dovuto codificare l'URL dell'API REST nel nostro codice frontend.

Definiamo anche le variabili page e language, che utilizzeremo per tenere traccia della paginazione e del filtro della lingua.

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

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

Aggiungiamo un gestore di eventi al pulsante per caricare i libri. Quando viene fatto clic, viene chiamata la funzione 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);
    });

Lo stesso vale per la casella di selezione: aggiungiamo un gestore di eventi per ricevere una notifica delle modifiche alla selezione della lingua. Come per il pulsante, chiamiamo anche la funzione appendMoreBooks(), passando l'URL dell'API REST, la pagina corrente e la selezione della lingua.

Diamo un'occhiata alla funzione che recupera e aggiunge i libri:

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

Sopra, stiamo creando l'URL esatto da utilizzare per chiamare l'API REST. Normalmente possiamo specificare tre parametri di query, ma in questa UI ne specifichiamo solo due:

  • page: un numero intero che indica la pagina corrente per la paginazione dei libri,
  • language: una stringa di lingua per filtrare in base alla lingua scritta.

Quindi, utilizziamo l'API Fetch per recuperare l'array JSON contenente i dettagli del libro.

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

A seconda della presenza o meno dell'intestazione Link nella risposta, mostreremo o nasconderemo il pulsante [More books...], poiché l'intestazione Link è un suggerimento che ci indica se ci sono altri libri da caricare (nell'intestazione Link sarà presente un URL next).

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

Nella sezione precedente della funzione, per ogni libro restituito dall'API REST, cloneremo il modello con alcuni componenti web che rappresentano un libro e riempiremo gli slot del modello con i dettagli del libro.

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

Per rendere il codice ISBN un po' più bello, utilizziamo la libreria JsBarcode per creare un bel codice a barre come quello sulla copertina posteriore dei libri reali.

Esecuzione e test dell'applicazione in locale

Per ora il codice è sufficiente, è il momento di vedere l'applicazione in azione. Innanzitutto, lo faremo localmente, all'interno di Cloud Shell, prima del deployment vero e proprio.

Installiamo i moduli NPM necessari alla nostra applicazione con:

$ npm install

Eseguiamo l'app con il solito comando:

$ npm start

Oppure con il ricaricamento automatico delle modifiche grazie a nodemon, con:

$ npm run dev

L'applicazione è in esecuzione localmente e possiamo accedervi dal browser all'indirizzo http://localhost:8080.

Deployment dell'applicazione App Engine

Ora che abbiamo la certezza che la nostra applicazione funziona correttamente a livello locale, è il momento di eseguirne il deployment su App Engine.

Per eseguire il deployment dell'applicazione, esegui questo comando:

$ gcloud app deploy -q

Dopo circa un minuto, l'applicazione dovrebbe essere implementata.

L'applicazione sarà disponibile a un URL nel formato: https://${GOOGLE_CLOUD_PROJECT}.appspot.com.

Esplorare la UI della nostra applicazione web App Engine

Ora puoi:

  • Fai clic sul pulsante [More books...] per caricare altri libri.
  • Seleziona una lingua specifica per visualizzare i libri solo in quella lingua.
  • Puoi cancellare la selezione con la piccola croce nella casella di selezione per tornare all'elenco di tutti i libri.

10. Liberare spazio (facoltativo)

Se non intendi conservare l'app, puoi liberare spazio dalle risorse per risparmiare sui costi ed essere un buon cittadino del cloud eliminando l'intero progetto:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

11. Complimenti!

Abbiamo creato un insieme di servizi, grazie a Cloud Functions, App Engine e Cloud Run, per esporre vari endpoint API web e frontend web, per archiviare, aggiornare e sfogliare una libreria di libri, seguendo alcuni buoni pattern di progettazione per lo sviluppo di API REST.

Argomenti trattati

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

Approfondimento

Se vuoi esplorare ulteriormente questo esempio concreto ed espanderlo, ecco un elenco di elementi che potresti voler esaminare:

  • Sfrutta API Gateway per fornire una facciata API comune alla funzione di importazione dei dati e al contenitore dell'API REST, per aggiungere funzionalità come la gestione delle chiavi API per accedere all'API o definire limiti di frequenza per i consumatori di API.
  • Esegui il deployment del modulo del nodo Swagger-UI nell'applicazione App Engine per documentare e offrire un ambiente di test per l'API REST.
  • Nel frontend, oltre alla funzionalità di navigazione esistente, aggiungi schermate aggiuntive per modificare i dati e creare nuove voci del libro. Inoltre, poiché utilizziamo il database Cloud Firestore, sfruttiamo la funzionalità in tempo reale per aggiornare i dati del libro visualizzati man mano che vengono apportate modifiche.