Workshop API Web Serverless

1. Ringkasan

Tujuan codelab ini adalah untuk mendapatkan pengalaman dengan layanan "tanpa server" yang ditawarkan oleh Google Cloud Platform:

  • Cloud Functions — untuk men-deploy unit kecil logika bisnis dalam bentuk fungsi, yang bereaksi terhadap berbagai peristiwa (pesan Pub/Sub, file baru di Cloud Storage, permintaan HTTP, dan lainnya),
  • App Engine — untuk men-deploy dan menayangkan aplikasi web, API web, backend seluler, aset statis, dengan kemampuan meningkatkan dan menurunkan skala dengan cepat,
  • Cloud Run — untuk men-deploy dan menskalakan container, yang dapat berisi bahasa, runtime, atau library apa pun.

Dan untuk menemukan cara memanfaatkan layanan tanpa server tersebut untuk men-deploy dan menskalakan Web dan REST API, sembari juga melihat beberapa prinsip desain RESTful yang baik.

Di workshop ini, kita akan membuat penjelajah rak buku yang terdiri dari:

  • Cloud Function: untuk mengimpor set data awal buku yang tersedia di library, dalam database dokumen Cloud Firestore,
  • Container Cloud Run: yang akan mengekspos REST API di atas konten database,
  • Frontend web App Engine: untuk menelusuri daftar buku, dengan memanggil REST API kami.

Tampilan frontend web akan terlihat seperti ini di akhir codelab ini:

b6964f26b9624565.png

Yang akan Anda pelajari

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

2. Penyiapan dan Persyaratan

Penyiapan lingkungan mandiri

  1. Login ke Cloud Console dan buat project baru atau gunakan kembali project yang sudah ada. (Jika belum memiliki akun Gmail atau Google Workspace, Anda harus membuatnya.)

96a9c957bc475304.png

b9a10ebdf5b5a448.png

a1e3c01a38fa61c2.png

Ingat project ID, nama unik di semua project Google Cloud (maaf, nama di atas telah digunakan dan tidak akan berfungsi untuk Anda!) Project ID tersebut selanjutnya akan dirujuk di codelab ini sebagai PROJECT_ID.

  1. Selanjutnya, Anda harus mengaktifkan penagihan di Cloud Console untuk menggunakan resource Google Cloud.

Menjalankan operasi dalam codelab ini seharusnya tidak memerlukan banyak biaya, bahkan mungkin tidak sama sekali. Pastikan untuk mengikuti petunjuk yang ada di bagian "Membersihkan" yang memberi tahu Anda cara menonaktifkan resource sehingga tidak menimbulkan penagihan di luar tutorial ini. Pengguna baru Google Cloud memenuhi syarat untuk mengikuti program Uji Coba Gratis seharga$300 USD.

Mulai Cloud Shell

Meskipun Google Cloud dapat dioperasikan dari jarak jauh dari laptop Anda, dalam codelab ini, Anda akan menggunakan Google Cloud Shell, lingkungan command line yang berjalan di Cloud.

Dari GCP Console, klik ikon Cloud Shell di toolbar kanan atas:

bce75f34b2c53987.png

Hanya perlu waktu beberapa saat untuk penyediaan dan terhubung ke lingkungan. Jika sudah selesai, Anda akan melihat tampilan seperti ini:

f6ef2b5f13479f3a.png

Mesin virtual ini berisi semua alat pengembangan yang Anda perlukan. Layanan ini menawarkan direktori beranda tetap sebesar 5 GB, dan berjalan di Google Cloud, sehingga sangat meningkatkan performa dan autentikasi jaringan. Semua pekerjaan Anda di lab ini dapat dilakukan hanya dengan browser.

3 Menyiapkan lingkungan dan mengaktifkan cloud API

Untuk menggunakan berbagai layanan yang kami perlukan di seluruh project, kami akan mengaktifkan beberapa API. Kami akan melakukannya dengan meluncurkan perintah berikut di Cloud Shell:

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

Setelah beberapa saat, Anda akan melihat operasi berhasil diselesaikan:

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

Kita juga akan menyiapkan variabel lingkungan yang akan diperlukan selama jalur: region cloud tempat fungsi, aplikasi, dan container akan di-deploy:

$ export REGION=europe-west3

Karena akan menyimpan data di database Cloud Firestore, kita perlu membuat database:

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

Selanjutnya dalam codelab ini, saat mengimplementasikan REST API, kita perlu mengurutkan dan memfilter data. Untuk itu, kita akan membuat tiga indeks:

$ 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

Ketiga indeks tersebut sesuai dengan penelusuran yang akan kami lakukan menurut penulis atau bahasa, sembari mempertahankan urutan dalam koleksi melalui bidang yang diperbarui.

4. Mendapatkan kode

Dapatkan kode dari repositori GitHub berikut:

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

Kode aplikasi ditulis menggunakan Node.JS.

Anda akan memiliki struktur folder berikut yang relevan untuk lab ini:

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

Berikut adalah folder yang relevan:

  • data — Folder ini berisi contoh data yang berisi daftar 100 buku.
  • function-import — Fungsi ini akan menawarkan endpoint untuk mengimpor data sampel.
  • run-crud — Penampung ini akan mengekspos Web API untuk mengakses data buku yang disimpan di Cloud Firestore.
  • appengine-frontend — Aplikasi web App Engine ini akan menampilkan frontend hanya baca yang sederhana untuk menjelajahi daftar buku.

5. Contoh data perpustakaan buku

Dalam folder data, kita memiliki file books.json yang berisi daftar seratus buku, yang mungkin layak dibaca. Dokumen JSON ini adalah array yang berisi objek JSON. Mari lihat bentuk data yang akan kita serahkan melalui Cloud Function:

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

Semua entri buku kami dalam array ini berisi informasi berikut:

  • isbn — Kode ISBN-13 yang mengidentifikasi buku.
  • author — Nama penulis buku.
  • language — Bahasa lisan yang digunakan dalam buku.
  • pages — Jumlah halaman dalam buku.
  • title — Judul buku.
  • year — Tahun buku dipublikasikan.

6. Endpoint fungsi untuk mengimpor data buku sampel

Di bagian pertama ini, kita akan mengimplementasikan endpoint yang akan digunakan untuk mengimpor data buku sampel. Kami akan menggunakan Cloud Functions untuk tujuan ini.

Menjelajahi kode

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

Dalam dependensi runtime, kita hanya memerlukan modul NPM @google-cloud/firestore untuk mengakses database dan menyimpan data buku. Di balik layar, runtime Cloud Functions juga menyediakan framework web Express, sehingga kita tidak perlu mendeklarasikannya sebagai dependensi.

Dalam dependensi pengembangan, kita mendeklarasikan Framework Fungsi (@google-cloud/functions-framework), yang merupakan framework runtime yang digunakan untuk memanggil fungsi. Ini adalah framework open source yang juga dapat Anda gunakan secara lokal di mesin (dalam kasus kami, di dalam Cloud Shell) untuk menjalankan fungsi tanpa men-deploy setiap kali Anda melakukan perubahan, sehingga meningkatkan loop masukan pengembangan.

Untuk menginstal dependensi, gunakan perintah install:

$ npm install

Skrip start menggunakan Framework Fungsi untuk memberi Anda perintah yang dapat digunakan untuk menjalankan fungsi secara lokal dengan petunjuk berikut:

$ npm start

Anda dapat menggunakan curl atau berpotensi pratinjau web Cloud Shell untuk permintaan HTTP GET guna berinteraksi dengan fungsi.

Sekarang mari kita lihat file index.js yang berisi logika fungsi impor data buku:

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

Kami membuat instance modul Firestore, dan mengarahkan ke koleksi buku (mirip dengan tabel dalam database relasional).

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

Kami mengekspor fungsi JavaScript parseBooks. Ini adalah fungsi yang akan kita deklarasikan saat kita men-deploy-nya nanti.

Beberapa petunjuk berikutnya memeriksa apakah:

  • Kami hanya menerima permintaan HTTP POST, dan menampilkan kode status 405 untuk menunjukkan bahwa metode HTTP lainnya tidak diizinkan.
  • Kami hanya menerima payload application/json, dan mengirimkan kode status 406 untuk menunjukkan bahwa ini bukan format payload yang dapat diterima.
    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()
        });
    }

Kemudian, kita dapat mengambil payload JSON melalui body permintaan. Kami sedang menyiapkan operasi kelompok Firestore untuk menyimpan semua buku secara massal. Kita melakukan iterasi pada array JSON yang terdiri dari detail buku, dengan menjelajahi kolom isbn, title, author, language, pages, dan year. Kode ISBN buku akan berfungsi sebagai kunci atau ID utama.

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

Setelah sebagian besar data siap, kita dapat melakukan operasi. Jika operasi penyimpanan gagal, kita menampilkan kode status 400 untuk memberi tahu bahwa operasi gagal. Jika tidak, kita dapat menampilkan respons OK dengan kode status 202 yang menunjukkan bahwa permintaan simpan massal telah diterima.

Menjalankan dan menguji fungsi impor

Sebelum menjalankan kode, kita akan menginstal dependensi dengan:

$ npm install

Untuk menjalankan fungsi secara lokal, berkat Framework Functions, kita akan menggunakan perintah skrip start yang ditentukan dalam package.json:

$ npm start

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

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

Untuk mengirim permintaan HTTP POST ke fungsi lokal, Anda dapat menjalankan:

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

Saat meluncurkan perintah ini, Anda akan melihat output berikut, yang mengonfirmasi bahwa fungsi berjalan secara lokal:

{"status":"OK"}

Anda juga dapat membuka UI Cloud Console untuk memeriksa apakah data memang disimpan di Firestore:

d6a2b31bfa3443f2.png

Dalam screenshot di atas, kita dapat melihat koleksi books yang dibuat, daftar dokumen buku yang diidentifikasi dengan kode ISBN buku, dan detail entri buku tersebut di sebelah kanan.

Men-deploy fungsi di cloud

Untuk men-deploy fungsi tersebut di Cloud Functions, kita akan menggunakan perintah berikut di direktori function-import:

$ gcloud functions deploy bulk-import \
         --trigger-http \
         --runtime=nodejs12 \
         --allow-unauthenticated \
         --region=${REGION} \
         --source=. \
         --entry-point=parseBooks

Kita men-deploy fungsi tersebut dengan nama simbolis bulk-import. Fungsi ini dipicu melalui permintaan HTTP. Kita menggunakan runtime Node.JS 12. Kita men-deploy fungsi secara publik (idealnya kita harus mengamankan endpoint tersebut). Kita menentukan region tempat kita ingin fungsi tersebut berada. Dan kita mengarahkan ke sumber dalam direktori lokal dan menggunakan parseBooks (fungsi JavaScript yang diekspor) sebagai titik entri.

Setelah beberapa menit atau kurang, fungsi akan di-deploy di cloud. Di UI Cloud Console, Anda akan melihat fungsi muncul:

c3156d50ba917ddd.png

Dalam output deployment, Anda seharusnya dapat melihat URL fungsi Anda, yang mengikuti konvensi penamaan tertentu (https://${REGION}-${GOOGLE_CLOUD_PROJECT}.cloudfunctions.net/${FUNCTION_NAME}), dan tentu saja, Anda juga dapat menemukan URL pemicu HTTP ini di UI Cloud Console, di tab pemicu:

2d19539de3de98eb.png

Anda juga dapat mengambil URL melalui command line dengan gcloud:

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

Mari kita simpan dalam variabel lingkungan BULK_IMPORT_URL agar kita dapat menggunakannya kembali untuk menguji fungsi yang di-deploy.

Menguji fungsi yang di-deploy

Dengan perintah curl serupa yang digunakan sebelumnya untuk menguji fungsi yang berjalan secara lokal, kita akan menguji fungsi yang di-deploy. Satu-satunya perubahan adalah URL:

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

Jika berhasil, jendela akan menampilkan output berikut:

{"status":"OK"}

Setelah fungsi impor kita diterapkan dan siap, kita telah mengupload sampel data. Sekarang saatnya kita mengembangkan REST API yang mengekspos set data ini.

7. Kontrak REST API

Meskipun kita tidak menetapkan kontrak API menggunakan, misalnya, spesifikasi Open API, kita akan melihat berbagai endpoint REST API.

API menukar buku objek JSON, yang terdiri dari:

  • isbn (opsional) — String 13 karakter yang mewakili kode ISBN yang valid,
  • authorString tidak kosong yang mewakili nama penulis buku,
  • languageString yang tidak kosong berisi bahasa penulisan buku,
  • pagesInteger positif untuk jumlah halaman buku,
  • titleString yang tidak kosong dengan judul buku,
  • year — nilai Integer untuk tahun penerbitan buku.

Contoh payload buku:

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

DAPATKAN /buku

Dapatkan daftar semua buku, yang mungkin difilter menurut penulis dan/atau bahasa, dan diberi nomor halaman berdasarkan jendela 10 hasil pada satu waktu.

Payload isi: tidak ada.

Parameter kueri:

  • author (opsional) — memfilter daftar buku berdasarkan pengarang,
  • language (opsional) — memfilter daftar buku berdasarkan bahasa,
  • page (opsional, default = 0) — menunjukkan peringkat halaman hasil yang akan ditampilkan.

Menampilkan: array JSON objek buku.

Kode status:

  • 200 — jika permintaan berhasil mengambil daftar buku,
  • 400 — jika terjadi error.

POST /books dan POST /books/{isbn}

Posting payload buku baru, baik dengan parameter jalur isbn (dalam hal ini kode isbn tidak diperlukan dalam payload buku) atau tanpa (dalam hal ini kode isbn harus ada di payload buku)

Payload isi: objek buku.

Parameter kueri: tidak ada.

Menampilkan: tidak ada.

Kode status:

  • 201 — saat buku berhasil disimpan,
  • 406 — jika kode isbn tidak valid,
  • 400 — jika terjadi error.

DAPATKAN /books/{isbn}

Mengambil buku dari perpustakaan, yang diidentifikasi oleh kode isbn, yang diteruskan sebagai parameter jalur.

Payload isi: tidak ada.

Parameter kueri: tidak ada.

Menampilkan: objek JSON buku, atau objek kesalahan jika buku tidak ada.

Kode status:

  • 200 — jika buku ditemukan dalam database,
  • 400 — jika terjadi error,
  • 404 — jika buku tidak dapat ditemukan,
  • 406 — jika kode isbn tidak valid.

PUT /books/{isbn}

Memperbarui buku yang sudah ada, yang diidentifikasi oleh isbn yang diteruskan sebagai parameter jalur.

Payload isi: objek buku. Hanya kolom yang memerlukan update yang dapat diteruskan, kolom lain bersifat opsional.

Parameter kueri: tidak ada.

Menampilkan: buku yang diperbarui.

Kode status:

  • 200 — saat buku berhasil diperbarui,
  • 400 — jika terjadi error,
  • 406 — jika kode isbn tidak valid.

HAPUS /buku/{isbn}

Menghapus buku yang sudah ada, yang diidentifikasi oleh isbn yang diteruskan sebagai parameter jalur.

Payload isi: tidak ada.

Parameter kueri: tidak ada.

Menampilkan: tidak ada.

Kode status:

  • 204 — saat buku berhasil dihapus,
  • 400 — jika terjadi error.

8 Men-deploy dan mengekspos REST API dalam container

Menjelajahi kode

Dockerfile

Mari kita mulai dengan melihat Dockerfile, yang akan bertanggung jawab untuk mem-build kode aplikasi kita dalam container:

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

Kami menggunakan Node.JS 14"langsing" gambar. Kami sedang mengerjakan direktori /usr/src/app. Kami menyalin file package.json (antara lain di bawah) yang menentukan dependensi kami. Kita menginstal dependensi dengan npm install, dengan menyalin kode sumber. Terakhir, kita akan menunjukkan cara menjalankan aplikasi ini, dengan perintah node index.js.

package.json Anda

Selanjutnya, kita dapat melihat file 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"
    }
}

Kita menentukan bahwa kita ingin menggunakan Node.JS 14, seperti halnya dengan Dockerfile.

Aplikasi API web kita bergantung pada:

  • Modul NPM Firestore untuk mengakses data buku dalam database,
  • Library cors untuk menangani permintaan CORS (Cross Origin Resource Sharing), karena REST API kami akan dipanggil dari kode klien frontend aplikasi App Engine kami,
  • Framework Express, yang akan menjadi framework web kami untuk merancang API,
  • Lalu, modul isbn3 yang membantu memvalidasi kode ISBN buku.

Kita juga menentukan skrip start, yang akan berguna untuk memulai aplikasi secara lokal, untuk tujuan pengembangan dan pengujian.

index.js

Mari kita lanjutkan ke bagian isi kode, dengan pembahasan mendalam tentang index.js:

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

Kita memerlukan modul Firestore, dan mereferensikan koleksi books, tempat data buku kami disimpan.

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

Kami menggunakan Express, sebagai framework web kami, untuk menerapkan REST API. Kita menggunakan modul body-parser untuk mengurai payload JSON yang ditukarkan dengan API kita.

Modul querystring berguna untuk memanipulasi URL. Hal ini akan terjadi saat kita membuat header Link untuk tujuan penomoran halaman (selengkapnya tentang hal ini akan dibahas nanti).

Selanjutnya, kita akan mengonfigurasi modul cors. Header eksplisit yang ingin kita teruskan melalui CORS akan dihapus, karena sebagian besar biasanya dihapus, tetapi di sini, kita ingin mempertahankan panjang dan jenis konten yang biasa, serta header Link yang akan ditentukan untuk penomoran halaman.

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

Kita akan menggunakan modul NPM isbn3 untuk mengurai dan memvalidasi kode ISBN, dan kami mengembangkan fungsi utilitas kecil yang akan mengurai kode ISBN, dan merespons dengan kode status 406 pada respons, jika kode ISBN tidak valid.

  • GET /books

Mari kita lihat endpoint GET /books, bagian demi bagian:

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

Kita bersiap untuk membuat kueri database, dengan menyiapkan kueri. Kueri ini akan bergantung pada parameter kueri opsional, untuk memfilter menurut penulis dan/atau menurut bahasa. Kami juga mengembalikan daftar buku dengan potongan 10 buku.

Jika terjadi error selama proses berlangsung, saat mengambil buku, kami akan menampilkan error dengan kode status 400.

Mari memperbesar bagian yang terpotong pada endpoint tersebut:

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

Di bagian sebelumnya, kita memfilter menurut author dan language, tetapi di bagian ini, kita akan mengurutkan daftar buku berdasarkan tanggal terakhir diperbarui (terakhir diperbarui muncul pertama). Dan kita juga akan memberi nomor pada hasilnya, dengan mendefinisikan batas (jumlah elemen yang akan ditampilkan), dan offset (titik awal untuk mengembalikan batch buku berikutnya).

Kita menjalankan kueri, mendapatkan snapshot data, dan menempatkan hasil tersebut dalam array JavaScript yang akan ditampilkan di akhir fungsi.

Mari selesaikan penjelasan tentang endpoint ini, dengan melihat praktik yang baik: menggunakan header Link untuk menentukan link URI ke halaman data pertama, sebelumnya, berikutnya, atau terakhir (dalam kasus ini, kita hanya akan memberikan berikutnya).

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

Logika pada awalnya mungkin terlihat sedikit rumit, tetapi yang kami lakukan adalah menambahkan link sebelumnya jika kami tidak berada di halaman pertama data. Kemudian, kita menambahkan link berikutnya jika halaman data penuh (yaitu berisi jumlah maksimum buku seperti yang ditentukan oleh konstanta PAGE_SIZE, dengan asumsi ada halaman lain yang muncul dengan lebih banyak data). Selanjutnya, kita menggunakan fungsi resource#links() Express untuk membuat header yang tepat dengan sintaksis yang tepat.

Sebagai informasi, header link akan terlihat seperti ini:

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

Kedua endpoint ada di sini untuk membuat buku baru. Satu meneruskan kode ISBN dalam payload buku, sedangkan yang lain meneruskannya sebagai parameter jalur. Kedua cara tersebut akan memanggil fungsi 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}`});
    }
}

Kita akan memeriksa kode isbn valid, jika tidak, akan menampilkan hasil dari fungsi (dan menyetel kode status 406). Kami mengambil kolom buku dari payload yang diteruskan dalam isi permintaan. Kemudian, kita akan menyimpan detail buku di Firestore. Menampilkan 201 jika berhasil, dan 400 jika gagal.

Jika berhasil kembali, kita juga menyetel header lokasi, untuk memberikan tanda kepada klien API tempat resource yang baru dibuat. Header akan terlihat seperti berikut:

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

Mari kita ambil buku yang diidentifikasi melalui ISBN-nya, dari 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}`});
    }
});

Seperti biasa, kami memeriksa apakah ISBN valid. Kita membuat kueri ke Firestore untuk mengambil buku. Properti snapshot.exists berguna untuk mengetahui apakah benar sebuah buku ditemukan. Jika tidak, kami akan mengirim pesan error dan kode status 404 Tidak Ditemukan. Kita mengambil data buku, dan membuat objek JSON yang mewakili buku tersebut, untuk dikembalikan.

  • PUT /books/:isbn

Kami menggunakan metode PUT untuk memperbarui buku yang sudah ada.

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

Kita memperbarui kolom tanggal/waktu updated untuk mengingat kapan terakhir kali kita memperbarui data tersebut. Kami menggunakan strategi {merge:true} yang mengganti kolom yang ada dengan nilai barunya (jika tidak, semua kolom akan dihapus, dan hanya kolom baru dalam payload yang akan disimpan, menghapus kolom yang ada dari update sebelumnya atau pembuatan awal).

Kita juga menyetel header Location agar mengarah ke URI buku.

  • DELETE /books/:isbn

Menghapus buku cukup mudah. Kita cukup memanggil metode delete() pada referensi dokumen. Kami akan menampilkan kode status 204 karena kami tidak akan menampilkan konten apa pun.

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

Memulai server Express / Node

Terakhir, kita memulai server, yang memantau port 8080 secara default:

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

Menjalankan aplikasi secara lokal

Untuk menjalankan aplikasi secara lokal, kita akan menginstal dependensi terlebih dahulu dengan:

$ npm install

Kemudian, kita dapat mulai dengan:

$ npm start

Server akan dimulai pada localhost dan memantau port 8080 secara default.

Anda juga dapat mem-build container Docker, dan juga menjalankan image container, dengan perintah berikut:

$ docker build -t crud-web-api .

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

Menjalankan dalam Docker juga merupakan cara yang bagus untuk memeriksa kembali apakah containerization aplikasi kita akan berjalan dengan baik selagi kita mem-build-nya di cloud dengan Cloud Build.

Menguji API

Terlepas dari cara menjalankan kode REST API (secara langsung melalui Node atau melalui image container Docker), kita sekarang dapat menjalankan beberapa kueri terhadap kode tersebut.

  • Buat buku baru (ISBN dalam payload isi):
$ curl -XPOST -d '{"isbn":"9782070368228","title":"Book","author":"me","pages":123,"year":2021,"language":"French"}' \
    -H "Content-Type: application/json" \
    http://localhost:8080/books
  • Buat buku baru (ISBN dalam parameter jalur):
$ curl -XPOST -d '{"title":"Book","author":"me","pages":123,"year":2021,"language":"French"}' \
    -H "Content-Type: application/json" \
    http://localhost:8080/books/9782070368228
  • Menghapus buku (yang kita buat):
$ curl -XDELETE http://localhost:8080/books/9782070368228
  • Mengambil buku berdasarkan ISBN:
$ curl http://localhost:8080/books/9780140449136
$ curl http://localhost:8080/books/9782070360536
  • Perbarui buku yang sudah ada dengan hanya mengubah judulnya:
$ curl -XPUT \
       -d '{"title":"Book"}' \
       -H "Content-Type: application/json" \
       http://localhost:8080/books/9780003701203
  • Ambil daftar buku (10 buku pertama):
$ curl http://localhost:8080/books
  • Cari buku yang ditulis oleh penulis tertentu:
$ curl http://localhost:8080/books?author=Virginia+Woolf
  • Daftar buku yang ditulis dalam bahasa Inggris:
$ curl http://localhost:8080/books?language=English
  • Muat halaman ke-4 dari buku:
$ curl http://localhost:8080/books?page=3

Kita juga dapat menggabungkan parameter kueri author, language, dan books untuk mempersempit penelusuran.

Membuat dan men-deploy REST API dalam container

Karena kami senang REST API berfungsi sesuai rencana, ini adalah waktu yang tepat untuk men-deploy-nya di Cloud, di Cloud Run.

Kita akan melakukannya dalam dua langkah:

  • Pertama, dengan mem-build image container dengan Cloud Build, dengan perintah berikut:
$ gcloud builds submit \
         --tag gcr.io/${GOOGLE_CLOUD_PROJECT}/crud-web-api
  • Kemudian, dengan men-deploy layanan dengan perintah kedua ini:
$ gcloud run deploy run-crud \
         --image gcr.io/${GOOGLE_CLOUD_PROJECT}/crud-web-api \
         --allow-unauthenticated \
         --region=${REGION} \
         --platform=managed

Dengan perintah pertama, Cloud Build mem-build image container dan menghostingnya di Container Registry. Perintah berikutnya men-deploy image container dari registry, dan men-deploy-nya di region cloud.

Di UI Cloud Console, periksa kembali apakah layanan Cloud Run kami kini muncul dalam daftar:

4ca13b0a703b2126.png

Satu langkah terakhir yang akan kita lakukan di sini adalah mengambil URL layanan Cloud Run yang baru di-deploy, berkat perintah berikut:

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

Kita memerlukan URL Cloud Run REST API di bagian berikutnya karena kode frontend App Engine kita akan berinteraksi dengan API.

9. Menghosting aplikasi web untuk menjelajahi koleksi

Bagian terakhir dari teka-teki untuk menambahkan glitter ke project ini adalah menyediakan frontend web yang akan berinteraksi dengan REST API kami. Untuk tujuan tersebut, kita akan menggunakan Google App Engine, dengan beberapa kode JavaScript klien yang akan memanggil API melalui permintaan AJAX (menggunakan API Fetch sisi klien).

Aplikasi kita, meskipun di-deploy di runtime Node.JS App Engine, sebagian besar terbuat dari resource statis. Kode backend tidak banyak, karena sebagian besar interaksi pengguna akan berada di browser melalui JavaScript sisi klien. Kita tidak akan menggunakan framework JavaScript frontend yang canggih, kita hanya akan menggunakan beberapa JavaScript "vanilla", dengan beberapa Komponen Web untuk UI menggunakan library komponen web Shoelace:

  • pilih kotak untuk memilih bahasa buku:

1b7bf64bd327b1ee.png

  • komponen kartu untuk menampilkan detail tentang buku tertentu (termasuk kode batang untuk mewakili ISBN buku, menggunakan library JsBarcode):

4dd54e4d5ee53367.png

  • dan tombol untuk memuat lebih banyak buku dari database:

4766c796a9d87475.png

Saat menggabungkan semua komponen visual tersebut secara bersamaan, halaman web yang dihasilkan untuk menjelajahi koleksi kami akan terlihat sebagai berikut:

fb6eae65811c8ac2.png

File konfigurasi app.yaml

Mari mulai menyelami code base aplikasi App Engine ini, dengan melihat file konfigurasi app.yaml-nya. Ini adalah file khusus untuk App Engine, dan memungkinkan konfigurasi berbagai hal seperti variabel lingkungan, berbagai "handler" aplikasi, atau menentukan bahwa beberapa resource adalah aset statis, yang akan yang ditayangkan oleh CDN bawaan 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

Kita menentukan bahwa aplikasi kita adalah Node.JS, dan kita ingin menggunakan versi 14.

Selanjutnya, kita menentukan variabel lingkungan yang mengarah ke URL layanan Cloud Run. Kita perlu memperbarui placeholder CHANGE_ME dengan URL yang benar (lihat di bawah tentang cara mengubahnya).

Setelah itu, kita menentukan berbagai pengendali. Tiga yang pertama mengarah ke lokasi kode sisi klien HTML, CSS, dan JavaScript, di bawah folder public/ dan subfoldernya. Yang keempat menunjukkan bahwa URL root dari aplikasi App Engine kita harus mengarah ke halaman index.html. Dengan begitu, kita tidak akan melihat akhiran index.html di URL saat mengakses root situs. Dan yang terakhir adalah URL default yang akan merutekan semua URL lainnya (/.*) ke aplikasi Node.JS kita (yaitu bagian "dinamis" dari aplikasi, berbeda dengan yang statis aset yang telah kami jelaskan).

Mari kita perbarui URL Web API dari layanan Cloud Run sekarang.

Dalam direktori appengine-frontend/, jalankan perintah berikut untuk memperbarui variabel lingkungan yang mengarah ke URL REST API berbasis Cloud Run:

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

Atau ubah string CHANGE_ME secara manual di app.yaml dengan URL yang benar:

env_variables:
    RUN_CRUD_SERVICE_URL: CHANGE_ME

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

Kita menekankan kembali bahwa kita ingin menjalankan aplikasi ini menggunakan Node.JS 14. Kami bergantung pada framework Express, serta modul NPM isbn3 untuk memvalidasi kode ISBN buku.

Dalam dependensi pengembangan, kita akan menggunakan modul nodemon untuk memantau perubahan file. Meskipun kami dapat menjalankan aplikasi secara lokal dengan npm start, membuat beberapa perubahan pada kode, menghentikan aplikasi dengan ^C, lalu meluncurkannya kembali, agak merepotkan. Sebagai gantinya, kita dapat menggunakan perintah berikut agar aplikasi dimuat ulang / dimulai ulang secara otomatis saat terjadi perubahan:

$ npm run dev

Kode 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());

Kami mewajibkan framework web Express. Kami menentukan bahwa direktori publik berisi aset statis yang dapat disajikan (setidaknya saat dijalankan secara lokal dalam mode pengembangan) oleh middleware static. Terakhir, kita memerlukan body-parser untuk mengurai payload JSON.

Mari kita lihat beberapa rute yang telah kita tentukan:

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

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

Yang pertama cocok dengan / akan dialihkan ke index.html di direktori public/html kami. Karena dalam mode pengembangan, kita tidak menjalankan dalam runtime App Engine, kita tidak mendapatkan pemilihan rute URL App Engine. Jadi di sini, kita hanya mengalihkan URL root ke file HTML.

Endpoint kedua yang kita tentukan /webapi akan menampilkan URL Cloud RUN REST API. Dengan begitu, kode JavaScript sisi klien akan mengetahui tempat untuk memanggil untuk mendapatkan daftar buku.

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

Untuk menyelesaikan, kami menjalankan aplikasi web Express dan mendengarkan di port 8080 secara default.

index.html Halaman

Kami tidak akan melihat setiap baris di halaman HTML yang panjang ini. Mari kita soroti beberapa baris kunci.

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

Dua baris pertama akan mengimpor library komponen web Shoelace (skrip dan stylesheet).

Baris berikutnya mengimpor library JsBarcode, untuk membuat kode batang kode ISBN buku.

Baris terakhir mengimpor kode JavaScript dan stylesheet CSS kita sendiri, yang terletak di subdirektori public/ kita.

Di body halaman HTML, kami menggunakan komponen Tali Sepatu dengan tag elemen kustomnya, seperti:

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

Kami juga menggunakan template HTML dan kemampuan pengisian slot untuk mewakili buku. Kami akan membuat salinan template tersebut untuk mengisi daftar buku, dan mengganti nilai di slot dengan detail buku:

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

Cukup HTML, kita hampir selesai meninjau kode. Satu bagian terakhir yang tersisa: kode JavaScript sisi klien app.js yang berinteraksi dengan REST API kami.

Kode JavaScript sisi klien app.js

Kita mulai dengan pemroses peristiwa level teratas yang menunggu konten DOM dimuat:

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

Setelah persiapan selesai, kita dapat menyiapkan beberapa konstanta dan variabel utama:

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

Pertama, kita akan mengambil URL REST API, berkat kode node App Engine yang menampilkan variabel lingkungan yang kita tetapkan di app.yaml pada awalnya. Berkat variabel lingkungan, endpoint /webapi, yang dipanggil dari kode sisi klien JavaScript, tidak harus meng-hardcode URL REST API di kode frontend kami.

Kita juga menentukan variabel page dan language, yang akan kita gunakan untuk melacak penomoran halaman dan pemfilteran bahasa.

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

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

Kita menambahkan pengendali peristiwa pada tombol untuk memuat buku. Jika diklik, fungsi akan memanggil fungsi 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);
    });

Demikian pula untuk kotak pilihan, kita menambahkan pengendali peristiwa agar diberi tahu tentang perubahan dalam pemilihan bahasa. Dan seperti tombolnya, kita juga memanggil fungsi appendMoreBooks(), meneruskan URL REST API, halaman saat ini, dan pilihan bahasa.

Jadi, mari kita lihat fungsi yang mengambil dan menambahkan buku:

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

Di atas, kita membuat URL persis yang akan digunakan untuk memanggil REST API. Ada tiga parameter kueri yang biasanya dapat kita tentukan, namun di UI ini, kita hanya menetapkan dua:

  • page — bilangan bulat yang menunjukkan halaman saat ini untuk penomoran halaman buku,
  • language — string bahasa yang akan difilter menurut bahasa yang ditulis.

Kemudian kami menggunakan Fetch API untuk mengambil array JSON yang berisi detail buku kami.

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

Bergantung pada apakah header Link ada di respons, kita akan menampilkan atau menyembunyikan tombol [More books...], karena header Link adalah petunjuk yang memberi tahu kita jika ada lebih banyak buku yang masih dimuat (akan ada menjadi URL next di header 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);
        ...
    }
}

Di bagian fungsi di atas, untuk setiap buku yang ditampilkan oleh REST API, kita akan meng-clone template dengan beberapa komponen web yang mewakili buku, dan kita mengisi slot template dengan detail buku.

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

Untuk membuat kode ISBN sedikit lebih menarik, kami menggunakan library JsBarcode untuk membuat barcode yang bagus seperti di sampul belakang buku asli!

Menjalankan dan menguji aplikasi secara lokal

Cukup kode untuk saat ini, saatnya melihat cara kerja aplikasi. Pertama, kita akan melakukannya secara lokal, dalam Cloud Shell, sebelum di-deploy secara nyata.

Kami menginstal modul NPM yang diperlukan oleh aplikasi kami dengan:

$ npm install

Dan kita akan menjalankan aplikasi seperti biasa:

$ npm start

Atau dengan pemuatan ulang perubahan otomatis berkat nodemon, dengan:

$ npm run dev

Aplikasi berjalan secara lokal, dan kita dapat mengaksesnya dari browser, di http://localhost:8080.

Men-deploy aplikasi App Engine

Setelah yakin bahwa aplikasi berjalan dengan baik secara lokal, kini saatnya untuk menerapkannya di App Engine.

Untuk men-deploy aplikasi, mari kita luncurkan perintah berikut:

$ gcloud app deploy -q

Setelah sekitar satu menit, aplikasi akan di-deploy.

Aplikasi akan tersedia di URL dengan bentuk: https://${GOOGLE_CLOUD_PROJECT}.appspot.com.

Menjelajahi UI aplikasi web App Engine kami

Kini Anda dapat:

  • Klik tombol [More books...] untuk memuat buku lainnya.
  • Pilih bahasa tertentu untuk hanya melihat buku dalam bahasa tersebut.
  • Anda dapat menghapus pilihan dengan tanda silang kecil di kotak pilihan, untuk kembali ke daftar semua buku.

10. Pembersihan (opsional)

Jika tidak ingin menyimpan aplikasi, Anda dapat membersihkan resource untuk menghemat biaya dan menjadi pengguna cloud yang baik secara keseluruhan dengan menghapus seluruh project:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT}

11. Selamat!

Kami membuat serangkaian layanan, berkat Cloud Functions, App Engine, dan Cloud Run, yang mengekspos berbagai endpoint Web API dan frontend web, untuk menyimpan, memperbarui, dan menjelajahi koleksi buku, dengan mengikuti beberapa pola desain yang baik untuk pengembangan REST API.

Yang telah kita bahas

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

Melangkah lebih jauh

Jika Anda ingin menjelajahi contoh konkret ini lebih lanjut dan memperluasnya, berikut daftar hal-hal yang mungkin ingin Anda selidiki:

  • Manfaatkan API Gateway untuk menyediakan fasad API umum ke fungsi impor data dan penampung REST API, untuk menambahkan fitur seperti menangani kunci API untuk mengakses API, atau menentukan batasan kapasitas bagi konsumen API.
  • Deploy modul node Swagger-UI di aplikasi App Engine untuk mendokumentasikan dan menawarkan playground untuk REST API.
  • Di depan, di luar kemampuan penjelajahan yang ada, tambahkan layar tambahan untuk mengedit data, buat entri buku baru. Selain itu, karena kita menggunakan database Cloud Firestore, manfaatkan fitur real-time untuk memperbarui data buku yang ditampilkan saat perubahan dibuat.