Sunucusuz Web API'leri Atölyesi

1. Genel Bakış

Bu codelab'in amacı "sunucusuz" deneyim sahibi olmaktır Google Cloud Platform tarafından sunulan hizmetler:

  • Cloud Functions: Çeşitli etkinliklere (Pub/Sub mesajları, Cloud Storage'daki yeni dosyalar, HTTP istekleri vb.) tepki veren işlevler şeklinde küçük iş mantığı birimleri dağıtmak için
  • App Engine: Hızlı ölçeklendirme yapma ve azaltma özelliklerine sahip web uygulamalarını, web API'lerini, mobil arka uçları, statik öğeleri dağıtmak ve sunmak için
  • Cloud Run: Herhangi bir dil, çalışma zamanı veya kitaplık içerebilen container'ları dağıtmak ve ölçeklendirmek için

Web ve REST API'lerini dağıtıp ölçeklendirirken bu sunucusuz hizmetlerden nasıl yararlanacağınızı, aynı zamanda bazı iyi RESTful tasarım ilkelerini nasıl kullanacağınızı keşfedin.

Bu atölyede şunlardan oluşan bir kitap rafı gezgini oluşturacağız:

  • Bir Cloud Functions işlevi: Kitaplığımızdaki kitaplardan oluşan ilk veri kümesini Cloud Firestore belge veritabanına aktarmak için
  • Cloud Run container'ı: Veritabanımızın içeriği üzerinde REST API açığa çıkarır.
  • App Engine web ön ucu: REST API'mizi çağırarak kitap listesine göz atmak için.

Bu codelab'in sonunda web ön ucu aşağıdaki gibi görünecek:

705e014da0ca5e90.png

Neler öğreneceksiniz?

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

2. Kurulum ve şartlar

Kendi hızınızda ortam kurulumu

  1. Google Cloud Console'da oturum açıp yeni bir proje oluşturun veya mevcut bir projeyi yeniden kullanın. Gmail veya Google Workspace hesabınız yoksa hesap oluşturmanız gerekir.

295004821bab6a87.png

37d264871000675d.png

96d86d3d5655cdbe.png

  • Proje adı, bu projenin katılımcıları için görünen addır. Google API'leri tarafından kullanılmayan bir karakter dizesidir. İstediğiniz zaman güncelleyebilirsiniz.
  • Proje Kimliği, tüm Google Cloud projelerinde benzersizdir ve değiştirilemez (belirlendikten sonra değiştirilemez). Cloud Console, otomatik olarak benzersiz bir dize oluşturur. bunun ne olduğunu umursamıyorsunuz. Çoğu codelab'de proje kimliğinizi (genellikle PROJECT_ID olarak tanımlanır) belirtmeniz gerekir. Oluşturulan kimliği beğenmezseniz rastgele bir kimlik daha oluşturabilirsiniz. Alternatif olarak, kendi ölçümünüzü deneyip mevcut olup olmadığına bakabilirsiniz. Bu adımdan sonra değiştirilemez ve proje süresince kalır.
  • Bilginiz olması açısından, bazı API'lerin kullandığı üçüncü bir değer, yani Proje Numarası daha vardır. Bu değerlerin üçü hakkında daha fazla bilgiyi belgelerde bulabilirsiniz.
  1. Sonraki adımda, Cloud kaynaklarını/API'lerini kullanmak için Cloud Console'da faturalandırmayı etkinleştirmeniz gerekir. Bu codelab'i çalıştırmanın maliyeti, yüksek değildir. Bu eğitim dışında faturalandırmanın tekrarlanmasını önlemek amacıyla kaynakları kapatmak için oluşturduğunuz kaynakları silebilir veya projeyi silebilirsiniz. Yeni Google Cloud kullanıcıları 300 ABD doları değerindeki ücretsiz denemeden yararlanabilir.

Cloud Shell'i başlatma

Google Cloud dizüstü bilgisayarınızdan uzaktan çalıştırılabilse de bu codelab'de, Cloud'da çalışan bir komut satırı ortamı olan Google Cloud Shell'i kullanacaksınız.

Google Cloud Console'da, sağ üstteki araç çubuğunda bulunan Cloud Shell simgesini tıklayın:

84688aa223b1c3a2.png

Ortamı sağlamak ve bağlamak yalnızca birkaç dakika sürer. Tamamlandığında şuna benzer bir sonuç görmeniz gerekir:

320e18fedb7fbe0.png

İhtiyacınız olan tüm geliştirme araçlarını bu sanal makinede bulabilirsiniz. 5 GB boyutunda kalıcı bir ana dizin sunar ve Google Cloud üzerinde çalışarak ağ performansını ve kimlik doğrulamasını büyük ölçüde iyileştirir. Bu codelab'deki tüm çalışmalarınız tarayıcıda yapılabilir. Herhangi bir şey yüklemeniz gerekmez.

3. Ortamı hazırlama ve Cloud API'lerini etkinleştirme

Bu proje boyunca ihtiyaç duyacağımız çeşitli hizmetleri kullanabilmek için birkaç API etkinleştireceğiz. Bu işlemi Cloud Shell'de aşağıdaki komutu başlatarak gerçekleştireceğiz:

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

Bir süre sonra işlemin başarıyla tamamlandığını göreceksiniz:

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

Ayrıca bu süreçte ihtiyaç duyacağımız bir ortam değişkeni de oluşturacağız: işlevimizi, uygulamamızı ve container'ı dağıtacağımız bulut bölgesi.

$ export REGION=europe-west3

Verileri Cloud Firestore veritabanında depolayacağımız için bu veritabanını oluşturmamız gerekecek:

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

Bu codelab'in ilerleyen bölümlerinde REST API'yi uygularken verileri sıralamamız ve filtrelememiz gerekecek. Bunun için üç dizin oluşturacağız:

$ 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 

Bu 3 dizin, yazara veya dile göre yapacağımız aramalara karşılık gelir ve koleksiyondaki güncellenmiş alanla sıralamayı korur.

4. Kodu alın

Aşağıdaki GitHub deposundan kodu alın:

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

Uygulama kodu, Node.JS kullanılarak yazılmıştır.

Bu laboratuvarla ilgili klasör yapınız aşağıdaki gibi olacaktır:

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

İlgili klasörler şunlardır:

  • data — Bu klasör, 100 kitaptan oluşan bir listenin örnek verilerini içerir.
  • function-import: Bu işlev, örnek verileri içe aktarmak için bir uç nokta sunar.
  • run-crud: Bu kapsayıcı, Cloud Firestore'da depolanan kitap verilerine erişmek için bir Web API'sini açığa çıkarır.
  • appengine-frontend: Bu App Engine web uygulaması, kitap listesine göz atmak için basit bir salt okunur ön uç görüntüler.

5. Örnek kitap kitaplığı verileri

Veri klasöründe, muhtemelen okumaya değer yüz kitabından oluşan listeyi içeren bir books.json dosyamız var. Bu JSON belgesi, JSON nesnelerini içeren bir dizidir. Cloud Functions işleviyle kullanacağımız verilerin şeklini inceleyelim:

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

Bu dizideki tüm kitap girişlerimiz aşağıdaki bilgileri içerir:

  • isbn — Kitabı tanımlayan ISBN-13 kodu.
  • author: Kitabın yazarının adı.
  • language — Kitabın yazıldığı konuşma dili.
  • pages: Kitaptaki sayfa sayısı.
  • title: Kitabın başlığı.
  • year — Kitabın yayınlandığı yıl.

6. Örnek kitap verilerini içe aktarmak için kullanılan bir işlev uç noktası

Bu ilk bölümde, örnek kitap verilerini içe aktarmak için kullanılacak uç noktayı uygulayacağız. Bunun için Cloud Functions'ı kullanacağız.

Kodu inceleyin

package.json dosyasını ele alarak başlayalım:

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

Çalışma zamanı bağımlılıklarında, veritabanına erişmek ve kitap verilerimizi depolamak için yalnızca @google-cloud/firestore NPM modülüne ihtiyacımız vardır. Temel olarak Cloud Functions çalışma zamanı Express web çerçevesini de sağlar. Böylece, bunu bağımlılık olarak tanımlamamız gerekmez.

Geliştirme bağımlılıklarında, işlevlerinizi çağırmak için kullanılan çalışma zamanı çerçevesi olan Functions Framework'ü (@google-cloud/functions-framework) tanımlıyoruz. Bu açık kaynak çerçeve, her değişiklik yaptığınızda dağıtım yapmadan işlevleri çalıştırmak ve böylece geliştirme geri bildirim döngüsünü iyileştirmek için makinenizde (bizim durumumuzda Cloud Shell'de) yerel olarak da kullanabileceğiniz bir açık kaynak çerçevedir.

Bağımlılıkları yüklemek için install komutunu kullanın:

$ npm install

start komut dosyası, size işlevi yerel olarak çalıştırmak üzere aşağıdaki talimatla birlikte kullanabileceğiniz bir komut vermek için Functions Çerçevesi'ni kullanır:

$ npm start

İşlevle etkileşime geçmek üzere HTTP GET istekleri için curl'ü veya Cloud Shell web önizlemesini kullanabilirsiniz.

Şimdi, kitap verilerini içe aktarma işlevimizin mantığını içeren index.js dosyasına bakalım:

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

Firestore modülünü örneklendiririz ve kitap koleksiyonunu işaret ederiz (ilişkisel veritabanlarındaki tabloya benzer).

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

parseBooks JavaScript işlevini dışa aktarıyoruz. Bu, daha sonra dağıttığımızda açıklayacağımız işlevdir.

Sonraki birkaç talimatta şunlar kontrol edilir:

  • Yalnızca HTTP POST isteklerini kabul ederiz. Aksi takdirde, diğer HTTP yöntemlerine izin verilmediğini belirtmek için 405 durum kodu döndürürüz.
  • Yalnızca application/json yüklerini kabul ediyoruz ve bunun kabul edilebilir bir yük biçimi olmadığını belirtmek için 406 durum kodu gönderiyoruz.
    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()
        });
    }

Ardından, isteğin body öğesi aracılığıyla JSON yükünü alabiliriz. Tüm kitapları toplu olarak depolamak için bir Firestore toplu işlemi hazırlıyoruz. Kitap ayrıntılarını içeren JSON dizisi üzerinde isbn, title, author, language, pages ve year alanlarını kullanarak yineleme yaparız. Kitabın ISBN kodu, kitabın birincil anahtarı veya tanımlayıcısı olarak işlev görür.

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

Artık toplu veriler hazır olduğuna göre işlemi gerçekleştirebiliriz. Depolama işlemi başarısız olursa işlemin başarısız olduğunu belirtmek için bir 400 durum kodu döndürürüz. Aksi takdirde, toplu kayıt isteğinin kabul edildiğini belirten 202 durum koduyla birlikte bir Tamam yanıtı döndürebiliriz.

İçe aktarma işlevini çalıştırma ve test etme

Kodu çalıştırmadan önce bağımlıları aşağıdaki öğelerle yükleyeceğiz:

$ npm install

İşlevi yerel olarak çalıştırmak için Functions Çerçevesi sayesinde package.json içinde tanımladığımız start komut dosyası komutunu kullanacağız:

$ npm start

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

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

Yerel işlevinize HTTP POST isteği göndermek için şu komutu çalıştırabilirsiniz:

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

Bu komutu başlattığınızda, işlevin yerel olarak çalıştığını onaylayan aşağıdaki çıkışı görürsünüz:

{"status":"OK"}

Verilerin gerçekten Firestore'da depolanıp depolanmadığını kontrol etmek için Cloud Console kullanıcı arayüzüne de gidebilirsiniz:

409982568cebdbf8.png

Yukarıdaki ekran görüntüsünde, oluşturulan books koleksiyonunu, kitap ISBN koduyla tanımlanan kitap belgelerinin listesini ve sağ tarafta söz konusu kitap girişinin ayrıntılarını görüyoruz.

İşlevi bulutta dağıtma

İşlevi Cloud Functions'da dağıtmak için function-import dizininde aşağıdaki komutu kullanacağız:

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

İşlevi bulk-import sembolik adıyla dağıtırız. Bu işlev HTTP istekleri aracılığıyla tetiklenir. Node.JS 20 çalışma zamanını kullanıyoruz. İşlevi herkese açık olarak dağıtırız (ideal olarak, bu uç noktanın güvenliğini sağlamalıyız). İşlevin bulunmasını istediğimiz bölgeyi belirtiriz. Ayrıca, yerel dizindeki kaynakları işaret ediyor ve giriş noktası olarak parseBooks (dışa aktarılan JavaScript işlevi) kullanıyoruz.

Birkaç dakika veya daha kısa bir süre sonra işlev buluta dağıtılır. Cloud Console kullanıcı arayüzünde şu işlev gösterilir:

c910875d4dc0aaa8.png

Dağıtım çıkışında, işlevinizin belirli bir adlandırma kuralına (https://${REGION}-${GOOGLE_CLOUD_PROJECT}.cloudfunctions.net/${FUNCTION_NAME}) uyan URL'sini görebilirsiniz. Elbette bu HTTP tetikleyici URL'sini Cloud Console kullanıcı arayüzündeki tetikleyici sekmesinde de bulabilirsiniz:

380ffc46eb56441e.png

URL'yi gcloud kullanarak komut satırı üzerinden de alabilirsiniz:

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

Bu değişkeni, dağıtılan işlevimizi test etmek amacıyla yeniden kullanmak için BULK_IMPORT_URL ortam değişkeninde depolayalım.

Dağıtılan işlevi test etme

İşlevin yerel olarak çalıştığını test etmek için daha önce kullandığımız bir curl komutuyla dağıtılan işlevi test edeceğiz. Tek değişiklik şu URL olacaktır:

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

Yine başarılı olursa aşağıdaki çıkışı döndürür:

{"status":"OK"}

İçe aktarma işlevimiz dağıtıldığına ve örnek verilerimizi yüklediğimize göre artık bu veri kümesini açığa çıkaran REST API'yi geliştirme zamanı geldi.

7. REST API sözleşmesi

Örneğin, Open API spesifikasyonu kullanarak bir API sözleşmesi tanımlamasak da REST API'mizin çeşitli uç noktalarına göz atacağız.

API exchange'leri aşağıdakilerden oluşan JSON nesnelerini rezerve eder:

  • isbn (isteğe bağlı): Geçerli bir ISBN kodunu temsil eden 13 karakterlik String
  • author — kitabın yazarının adını temsil eden boş olmayan bir String,
  • language — kitabın yazıldığı dili içeren boş olmayan bir String,
  • pages — kitabın sayfa sayısı için pozitif Integer,
  • title — kitabın başlığını içeren boş bir String,
  • year — Kitabın yayın yılı için Integer değeri.

Örnek kitap yükü:

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

/books'u edinin

Yazara ve/veya dile göre filtrelenmiş ve tek seferde 10 sonuç pencerelerine göre sayfalara ayrılmış tüm kitapların listesini alın.

Vücut yükü: Yok.

Sorgu parametreleri:

  • author (isteğe bağlı) — kitap listesini yazara göre filtreler,
  • language (isteğe bağlı) — kitap listesini dile göre filtreler,
  • page (isteğe bağlı, varsayılan = 0) — Döndürülecek sonuç sayfasının sıralamasını gösterir.

İadeler: Kitap nesnelerinden oluşan bir JSON dizisi.

Durum kodları:

  • 200 — istek, kitap listesini getirmeyi başardığında,
  • 400 - Bir hata oluşursa.

/kitaplar ve POST /books/{isbn} YAYINLA

isbn yol parametresiyle (bu durumda, kitap yükünde isbn koduna gerek yoktur) veya içermeyen (bu durumda, kitap yükünde isbn kodu bulunmalıdır) yeni bir kitap yükü yayınlayın

Gövde yükü: Bir kitap nesnesi.

Sorgu parametreleri: yok.

Şunu döndürür: Hiçbir şey.

Durum kodları:

  • 201 — kitap başarıyla depolandığında,
  • 406: isbn kodu geçersizse
  • 400 - Bir hata oluşursa.

/books/{isbn} AL

Kitaplıktan isbn koduyla tanımlanan ve yol parametresi olarak iletilen bir kitabı alır.

Vücut yükü: Yok.

Sorgu parametreleri: yok.

Bir kitabın JSON nesnesi veya kitap yoksa bir hata nesnesi döndürür.

Durum kodları:

  • 200 — Kitap, veritabanında bulunursa
  • 400 — Bir hata oluşursa
  • 404 — Kitap bulunamazsa
  • 406isbn kodu geçersizse.

PUT /books/{isbn}

Yol parametresi olarak iletilen isbn ile tanımlanan mevcut bir kitabı günceller.

Gövde yükü: Bir kitap nesnesi. Yalnızca güncellenmesi gereken alanlar aktarılabilir. Diğer alanlar isteğe bağlıdır.

Sorgu parametreleri: yok.

Şunu döndürür: güncellenen kitap.

Durum kodları:

  • 200 — kitap başarıyla güncellendiğinde,
  • 400 — Bir hata oluşursa
  • 406isbn kodu geçersizse.

/books/{isbn} SİLİN

Yol parametresi olarak iletilen isbn ile tanımlanmış mevcut bir kitabı siler.

Vücut yükü: Yok.

Sorgu parametreleri: yok.

Şunu döndürür: Hiçbir şey.

Durum kodları:

  • 204 — kitap başarıyla silindiğinde,
  • 400 - Bir hata oluşursa.

8. Container'da REST API dağıtma ve kullanıma sunma

Kodu inceleyin

Dockerfile

Öncelikle, uygulama kodumuzu container mimarisine almaktan sorumlu olacak Dockerfile öğesine bakalım:

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

Bir Node.JS 20 "slim" görüntüsü kullanıyoruz. /usr/src/app dizininde çalışıyoruz. Diğer unsurların yanı sıra bağımlılıklarımızı tanımlayan package.json dosyasını (ayrıntılar aşağıda verilmiştir) kopyalıyoruz. Bağımlılıkları, kaynak kodunu kopyalayarak npm install ile yükleriz. Son olarak, node index.js komutuyla bu uygulamanın nasıl çalıştırılacağını belirtiriz.

package.json

Sonra, package.json dosyasına bakabiliriz:

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

Dockerfile ürününde olduğu gibi, Node.JS 14'ü kullanmak istediğimizi belirtiriz.

Web API uygulamamız aşağıdakilere bağlıdır:

  • Veritabanındaki kitap verilerine erişmek için Firestore NPM modülü
  • REST API'miz App Engine web uygulaması ön ucumuzun istemci kodundan çağrılacağı için CORS (Kaynaklar Arası Kaynak Paylaşımı) isteklerini işleyecek cors kitaplığı,
  • API'mizi tasarlamak için web çerçevemiz olacak olan Express çerçevesi,
  • Ardından, kitap ISBN kodlarını doğrulamaya yardımcı olan isbn3 modülü.

Ayrıca, geliştirme ve test amacıyla, uygulamayı yerel olarak başlatmak için kullanışlı olacak start komut dosyasını da belirtiriz.

index.js

Şimdi, index.js konusunu yakından inceleyerek kodun temeline geçelim:

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

Firestore modülüne ihtiyacımız var ve kitap verilerimizin saklandığı books koleksiyonuna referans veriyoruz.

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

REST API'mizi uygulamak için web çerçevemiz olarak Express'i kullanıyoruz. API'mizle değiştirilen JSON yüklerini ayrıştırmak için body-parser modülünü kullanıyoruz.

querystring modülü, URL'leri değiştirme konusunda faydalıdır. Bu durum, sayfalara ayırma amacıyla Link başlıkları oluşturduğumuzda mümkün olacaktır (ileride bu konuda daha fazla bilgi verilecektir).

Ardından cors modülünü yapılandırırız. CORS aracılığıyla aktarılmasını istediğimiz başlıkları açıkça belirtiriz, çünkü çoğu genellikle kaldırılmış olur, ancak burada normal içerik uzunluğu ve türünün yanı sıra sayfalara ayırma için belirteceğimiz Link başlığını korumak istiyoruz.

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

ISBN kodlarını ayrıştırıp doğrulamak için isbn3 NPM modülünü kullanacağız. Ayrıca, ISBN kodları geçersizse ISBN kodlarını ayrıştıracak ve yanıta 406 durum koduyla yanıt verecek küçük bir yardımcı program işlevi geliştireceğiz.

  • GET /books

GET /books uç noktasına tek tek göz atalım:

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

Bir sorgu hazırlayarak veritabanını sorgulamaya hazırlanıyoruz. Bu sorgu, yazara ve/veya dile göre filtrelemek için isteğe bağlı sorgu parametrelerine bağlı olacak. Ayrıca, 10 kitaplık parçalar halinde kitap listesini iade ediyoruz.

Bu işlem sırasında bir hata oluşursa kitaplar getirilirken 400 durum kodu içeren bir hata döndürülür.

Bu uç noktanın kırpılmış bölümünü yakından inceleyelim:

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

Önceki bölümde author ve language ölçütlerine göre filtreleme yaptık ancak bu bölümde, kitap listesini son güncelleme tarihine göre sıralayacağız (son güncelleme önce gelir). Ayrıca, bir sınır (döndürülecek öğe sayısı) ve ofset (bir sonraki kitap grubunun döndürüleceği başlangıç noktası) tanımlayarak sonucu sayfalara ayıracağız.

Sorguyu yürütür, verilerin anlık görüntüsünü alırız ve bu sonuçları, işlevin sonunda döndürülecek bir JavaScript dizisine yerleştiririz.

İyi bir uygulamaya göz atarak bu uç noktanın açıklamalarını tamamlayalım: Verinin ilk, önceki, sonraki veya son sayfalarına giden URI bağlantılarını tanımlamak için Link başlığını kullanmak (bu durumda yalnızca önceki ve sonrakini sağlayacağız).

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

Buradaki mantık ilk başta biraz karmaşık görünebilir, ancak verilerin ilk sayfasında değilsek önceki bağlantıyı eklemeyi planlıyoruz. Veri sayfası doluysa (yani PAGE_SIZE sabit değeri ile tanımlanan maksimum kitap sayısını içeriyorsa, daha fazla verinin ortaya çıktığını varsayarsak) bir next bağlantısı ekleriz. Ardından, doğru söz dizimiyle doğru başlığı oluşturmak için Express'in resource#links() işlevini kullanırız.

Bağlantı üstbilgisinin görünümü aşağıdaki gibi olacaktır:

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

Her iki uç nokta da yeni bir kitap oluşturmak için kullanılır. Biri kitap yükünde ISBN kodunu, diğeri ise yol parametresi olarak iletir. Her iki durumda da createBook() fonksiyonumuzu çağırın:

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

isbn kodunun geçerli olup olmadığını kontrol ederiz. Aksi takdirde, işlevden döneriz (ve 406 durum kodu ayarlarız). Kitap alanlarını, isteğin gövdesinde iletilen yükten alırız. Ardından kitap ayrıntılarını Firestore'da saklayacağız. Başarılı olarak 201, başarısızlık durumunda ise 400 geri dönüyor.

Başarıyla geri döndüğünde, yeni oluşturulan kaynağın bulunduğu API istemcisine ipuçları vermek için konum başlığını da ayarlarız. Başlık aşağıdaki gibi görünür:

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

ISBN'si ile tanımlanan bir kitabı Firestore'dan getirelim.

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

Her zaman olduğu gibi ISBN'nin geçerli olup olmadığını kontrol ederiz. Kitabı almak için Firestore'a bir sorgu göndeririz. snapshot.exists özelliği, gerçekten bir kitabın bulunup bulunmadığını anlamak açısından kullanışlıdır. Aksi takdirde, hata mesajı ve 404 Bulunamadı durum kodu gönderilir. Kitap verilerini alırız ve döndürülecek kitabı temsil eden bir JSON nesnesi oluştururuz.

  • PUT /books/:isbn

Mevcut bir kitabı güncellemek için PUT yöntemini kullanıyoruz.

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

updated tarih/saat alanını, bu kaydı en son ne zaman güncellediğimizi hatırlamak için güncelleriz. Mevcut alanları yeni değerlerle değiştiren {merge:true} stratejisini kullanırız (aksi takdirde tüm alanlar kaldırılır ve yalnızca yükteki yeni alanlar kaydedilerek önceki güncellemedeki veya ilk oluşturmadaki mevcut alanlar silinir).

Ayrıca Location başlığını, kitabın URI'sını işaret edecek şekilde ayarladık.

  • DELETE /books/:isbn

Kitapları silmek oldukça kolaydır. Belge referansında yalnızca delete() yöntemini çağırıyoruz. Herhangi bir içerik döndürmediğimiz için bir 204 durum kodu döndürürüz.

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

Ekspres / Düğüm sunucusunu başlatma

Son olarak, varsayılan olarak 8080 bağlantı noktasını dinleyerek sunucuyu başlatırız:

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

Uygulamayı yerel olarak çalıştırma

Uygulamayı yerel olarak çalıştırmak için öncelikle bağımlılıkları şu uygulamalarla yükleyeceğiz:

$ npm install

Şimdi şöyle başlayabiliriz:

$ npm start

Sunucu varsayılan olarak localhost itibarıyla başlatılır ve 8080 numaralı bağlantı noktasında dinleme yapar.

Aşağıdaki komutları kullanarak Docker container'ı derleyebilir ve container görüntüsünü çalıştırabilirsiniz:

$ docker build -t crud-web-api .

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

Docker'da çalışmak, uygulamamızı Cloud Build ile bulutta derlerken container mimarisine alma işleminin düzgün çalışıp çalışmadığını tekrar kontrol etmenin mükemmel bir yoludur.

API'yi test etme

REST API kodunu doğrudan Düğüm veya Docker container görüntüsü üzerinden nasıl çalıştırdığımıza bakılmaksızın birkaç sorgu çalıştırabiliyoruz.

  • Yeni bir kitap oluşturun (gövde yükünde ISBN):
$ curl -XPOST -d '{"isbn":"9782070368228","title":"Book","author":"me","pages":123,"year":2021,"language":"French"}' \
    -H "Content-Type: application/json" \
    http://localhost:8080/books
  • Yeni bir kitap oluşturun (yol parametresinde ISBN):
$ curl -XPOST -d '{"title":"Book","author":"me","pages":123,"year":2021,"language":"French"}' \
    -H "Content-Type: application/json" \
    http://localhost:8080/books/9782070368228
  • Bir kitabı (oluşturduğumuz) silme:
$ curl -XDELETE http://localhost:8080/books/9782070368228
  • ISBN'ye göre kitap alma:
$ curl http://localhost:8080/books/9780140449136
$ curl http://localhost:8080/books/9782070360536
  • Mevcut bir kitabı yalnızca başlığını değiştirerek güncelleme:
$ curl -XPUT \
       -d '{"title":"Book"}' \
       -H "Content-Type: application/json" \      
       http://localhost:8080/books/9780003701203
  • Kitap listesini (ilk 10) al:
$ curl http://localhost:8080/books
  • Belirli bir yazar tarafından yazılmış kitapları bulmak için:
$ curl http://localhost:8080/books?author=Virginia+Woolf
  • İngilizce yazılmış kitapları listeleyin:
$ curl http://localhost:8080/books?language=English
  • Kitapların 4. sayfasını yükle:
$ curl http://localhost:8080/books?page=3

Ayrıca, aramamızı daraltmak için author, language ve books sorgu parametrelerini birleştirebiliriz.

Container mimarisine alınmış REST API'yi oluşturma ve dağıtma

REST API'nin plana göre çalıştığından mutluluk duyuyoruz. Bu, REST API'yi Cloud Run'da Cloud'da dağıtmak için doğru zamandır.

Bunu iki adımda gerçekleştireceğiz:

  • İlk olarak aşağıdaki komutla Cloud Build ile container görüntüsünü oluşturun:
$ gcloud builds submit \
         --tag gcr.io/${GOOGLE_CLOUD_PROJECT}/crud-web-api
  • Ardından, hizmeti şu ikinci komutla dağıtarak:
$ gcloud run deploy run-crud \
         --image gcr.io/${GOOGLE_CLOUD_PROJECT}/crud-web-api \
         --allow-unauthenticated \
         --region=${REGION} \
         --platform=managed

Cloud Build, ilk komutla container görüntüsünü oluşturur ve Container Registry'de barındırır. Sıradaki komut, kayıt defterinden container görüntüsünü dağıtır ve bulut bölgesinde dağıtır.

Cloud Console kullanıcı arayüzünde, Cloud Run hizmetimizin artık listede yer alıp almadığını tekrar kontrol edebiliriz:

f62fbca02a8127c0.png

Burada da uygulayacağımız son adım, aşağıdaki komut sayesinde yeni dağıtılan Cloud Run hizmetinin URL'sini almaktır:

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

App Engine ön uç kodumuz API ile etkileşime gireceğinden sonraki bölümde Cloud Run REST API'mizin URL'sine ihtiyacımız olacak.

9. Kitaplığa göz atmak için bir web uygulaması barındırın

Yapbozun bu projeyi iyileştirecek son parçası, REST API'mizle etkileşim kuracak bir web ön ucu sağlamak. Bu amaçla Google App Engine'i, AJAX istekleri aracılığıyla (istemci tarafı Fetch API'sini kullanarak) API'yi çağıracak bazı istemci JavaScript kodlarıyla kullanacağız.

Uygulamamız, Node.JS App Engine çalışma zamanında dağıtılmış olsa da çoğunlukla statik kaynaklardan oluşuyor. Kullanıcı etkileşiminin büyük kısmı istemci taraflı JavaScript üzerinden tarayıcıda gerçekleşeceğinden fazla arka uç kodu yoktur. Gösterişli ön uç JavaScript çerçevesi kullanmayacağız. Yalnızca Shoelace web bileşeni kitaplığını kullanarak kullanıcı arayüzü için birkaç Web Bileşeni ile birlikte "vanilla" JavaScript kullanacağız:

  • kitabın dilini seçmek için bir seçim kutusu:

6fb9f741000a2dc1.png

  • Belirli bir kitapla ilgili ayrıntıları görüntülemek için bir kart bileşeni (JsBarcode kitaplığını kullanarak kitabın ISBN'sini temsil eden bir barkod dahil):

3aa21a9e16e3244e.png

  • ve veritabanından daha fazla kitap yüklemek için bir düğme:

3925ad81c91bbac9.png

Tüm bu görsel bileşenleri bir araya getirdiğimizde, kitaplığımıza göz atabileceğiniz web sayfası aşağıdaki gibi görünecektir:

18a5117150977d6.png

app.yaml yapılandırma dosyası

app.yaml yapılandırma dosyasına bakarak bu App Engine uygulamasının kod tabanını incelemeye başlayalım. Bu, App Engine'e özel bir dosyadır ve ortam değişkenleri, uygulamanın çeşitli "işleyicileri" gibi şeyleri yapılandırmanızı veya bazı kaynakların App Engine'in yerleşik CDN'si tarafından sunulacak statik öğeler olduğunu belirtmenizi sağlar.

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

Uygulamamızın Node.JS olduğunu ve sürüm 14'ü kullanmak istediğimizi belirtiyoruz.

Ardından, Cloud Run hizmet URL'sini işaret eden bir ortam değişkeni tanımlıyoruz. CHANGE_ME yer tutucusunu doğru URL ile güncellememiz gerekir (bunu nasıl değiştireceğinizi aşağıdan öğrenebilirsiniz).

Bundan sonra, çeşitli işleyiciler tanımlarız. İlk 3'ü, public/ klasörü ve alt klasörleri altındaki HTML, CSS ve JavaScript istemci tarafı kod konumunu gösteriyor. Dördüncüsü, App Engine uygulamamızın kök URL'sinin index.html sayfasına yönlendirmesi gerektiğini belirtir. Bu şekilde, web sitesinin kök dizinine erişirken URL'de index.html son ekini görmeyiz. Sonuncusu ise varsayılan olarak diğer tüm URL'leri (/.*) Node.JS uygulamamıza (açıkladığımız statik öğelerin aksine, uygulamanın "dinamik" bölümü) yönlendirir.

Şimdi Cloud Run hizmetinin Web API URL'sini güncelleyelim.

appengine-frontend/ dizininde, Cloud Run tabanlı REST API'mizin URL'sini gösteren ortam değişkenini güncellemek için aşağıdaki komutu çalıştırın:

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

Alternatif olarak, app.yaml içindeki CHANGE_ME dizesini doğru URL ile manuel olarak değiştirin:

env_variables:
    RUN_CRUD_SERVICE_URL: CHANGE_ME

Node.JS package.json dosyası

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

Bu uygulamayı Node.JS 14 kullanarak çalıştırmak istediğimizi tekrar vurgulamak isteriz. Kitapların doğrulanması için Express çerçevesinin yanı sıra isbn3 NPM modülüne güveniyoruz ISBN kodları.

Geliştirme bağımlılıklarında, dosya değişikliklerini izlemek için nodemon modülünü kullanacağız. Uygulamamızı npm start ile yerel olarak çalıştırabilsek de kodda bazı değişiklikler yapabilir, uygulamayı ^C ile durdurup yeniden başlatabiliriz. Ancak bu işlem biraz can sıkıcı. Bunun yerine, değişiklikler yapıldıktan sonra uygulamanın otomatik olarak yeniden yüklenmesini / yeniden başlatılmasını sağlamak için aşağıdaki komutu kullanabiliriz:

$ npm run dev

index.js Node.JS kodu

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

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

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

Express web çerçevesi gereklidir. Genel dizinin, static ara yazılımı tarafından sunulabilecek (en azından geliştirme modunda yerel olarak çalışırken) statik öğeler içerdiğini belirtiriz. Son olarak, JSON yüklerimizin ayrıştırılması için body-parser gerekir.

Tanımladığımız iki rotaya göz atalım:

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

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

/ ile eşleşen ilk öğe, public/html dizinimizdeki index.html öğesine yönlendirir. Geliştirme modunda App Engine çalışma zamanı içinde çalışmadığımızdan, App Engine'in URL yönlendirmesini devreye sokmuyoruz. Bunun yerine burada kök URL'yi HTML dosyasına yönlendiriyoruz.

/webapi tanımladığımız ikinci uç nokta, Cloud RUN REST API'mizin URL'sini döndürür. Bu şekilde, istemci taraflı JavaScript kodu, kitap listesini almak için nereye çağrılacağını bilir.

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

İşlemi tamamlamak için Express web uygulamasını çalıştırıyoruz ve varsayılan olarak 8080 numaralı bağlantı noktasından dinliyoruz.

index.html sayfası

Bu uzun HTML sayfasının her satırına bakmayacağız. Bunun yerine bazı önemli satırları vurgulayalım.

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

İlk iki satır, Ayakkabı Bağcığı web bileşeni kitaplığını (bir komut dosyası ve bir stil sayfası) içe aktarır.

Sonraki satır, kitap ISBN kodlarının barkodlarını oluşturmak için JsBarcode kitaplığını içe aktarır.

Son satırlar, public/ alt dizinlerimizde bulunan kendi JavaScript kodumuzu ve CSS stil sayfamızı içe aktarıyor.

HTML sayfasının body bölümünde Ayakkabı Bağcığı bileşenlerini özel öğe etiketleriyle kullanırız. Örneğin:

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

Ayrıca, bir kitabı temsil etmek için HTML şablonlarını ve bunların yuvayı doldurma özelliğini de kullanıyoruz. Kitap listesini doldurmak için bu şablonun kopyalarını oluşturur ve alanlardaki değerleri kitapların ayrıntılarıyla değiştiririz:

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

Bu kadar HTML yeterliyse kodu incelememiz bitmek üzere. Kalan son bir önemli bölüm: REST API'mizle etkileşime giren istemci taraflı app.js JavaScript kodu.

app.js istemci tarafı JavaScript kodu

DOM içeriğinin yüklenmesini bekleyen üst düzey bir etkinlik işleyici ile başlıyoruz:

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

Hazır olduğunda bazı önemli sabit değerler ve değişkenler oluşturabiliriz:

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

Öncelikle, başlangıçta app.yaml içinde ayarladığımız ortam değişkenini döndüren App Engine düğüm kodumuz sayesinde REST API'mizin URL'sini getireceğiz. JavaScript istemci tarafı kodundan çağrılan /webapi uç noktası sayesinde ortam değişkeni sayesinde REST API URL'sini ön uç kodumuza gömmemiz gerekmedi.

Ayrıca, sayfalara ayırma ve dil filtrelemeyi izlemek için kullanacağımız page ve language değişkenlerini de tanımlıyoruz.

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

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

Kitapları yüklemek için düğmeye bir etkinlik işleyici ekledik. Tıklandığında appendMoreBooks() işlevi çağrılır.

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

Seçim kutusuna benzer şekilde, dil seçimindeki değişikliklerin bildirilmesi için bir etkinlik işleyici ekleriz. Düğmede olduğu gibi appendMoreBooks() işlevini de çağırarak REST API URL'sini, geçerli sayfayı ve dil seçimini iletiriz.

Şimdi kitapları getiren ve ekleyen fonksiyona bir göz atalım:

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

Yukarıda, REST API'yi çağırmak için kullanılacak tam URL'yi oluşturuyoruz. Normalde belirtebileceğimiz üç sorgu parametresi vardır, ancak burada bu kullanıcı arayüzünde yalnızca iki parametre belirtilmektedir:

  • page - Kitapların sayfalara ayrılması için geçerli sayfayı gösteren bir tam sayı
  • language: Yazı diline göre filtrelenen bir dil dizesi.

Daha sonra, kitap ayrıntılarını içeren JSON dizisini almak için Getirme API'sini kullanırız.

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

Yanıtta Link başlığının mevcut olup olmadığına bağlı olarak, [More books...] düğmesini gösteririz veya gizleriz. Link başlığı, yüklenmeye devam edecek daha fazla kitap olup olmadığını bize bildiren bir ipucudur (Link başlığında next URL'si bulunur).

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

İşlevin yukarıdaki bölümünde, REST API'si tarafından döndürülen her kitap için şablonu, bir kitabı temsil eden bazı web bileşenleriyle klonlayacak, şablondaki alanları kitabın ayrıntılarıyla dolduracağız.

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

ISBN kodunu daha hoş hale getirmek için, gerçek kitapların arka kapağına benzer güzel bir barkod oluşturmak üzere JsBarcode kitaplığını kullanıyoruz.

Uygulamayı yerel olarak çalıştırma ve test etme

Şimdilik bu kadar kod yeterli. Şimdi sıra, uygulamanın nasıl çalıştığını görmeye geldi. İlk olarak, gerçek bir dağıtım yapmadan önce yerel olarak Cloud Shell'de bu işlemi gerçekleştireceğiz.

Uygulamamızın ihtiyaç duyduğu NPM modüllerini şunlarla yüklüyoruz:

$ npm install

Uygulamayı her zamanki gibi çalıştırırız:

$ npm start

Dilerseniz nodemon sayesinde değişikliklerin otomatik olarak yeniden yüklenmesini sağlayabilirsiniz. Bu özellik şunları içerir:

$ npm run dev

Uygulama yerel olarak çalışıyor ve http://localhost:8080 adresine giderek uygulamaya tarayıcıdan erişebiliyoruz.

App Engine uygulamasını dağıtma

Uygulamamızın yerel olarak düzgün çalıştığından emin olduğumuza göre şimdi App Engine'de dağıtmanın zamanı geldi.

Uygulamayı dağıtmak için şu komutu çalıştıralım:

$ gcloud app deploy -q

Yaklaşık bir dakika sonra uygulamanın dağıtılması gerekir.

Uygulama, https://${GOOGLE_CLOUD_PROJECT}.appspot.com şeklinin URL'sinde kullanılabilir olacaktır.

App Engine web uygulamamızın kullanıcı arayüzünü keşfetme

Artık şunları yapabilirsiniz:

  • Daha fazla kitap yüklemek için [More books...] düğmesini tıklayın.
  • Belirli bir dili seçerek yalnızca o dildeki kitapları görebilirsiniz.
  • Tüm kitapların listesine dönmek için seçimi, seçim kutusundaki küçük çarpı ile temizleyebilirsiniz.

10. Temizleme (isteğe bağlı)

Uygulamayı tutmak istemiyorsanız projenin tamamını silerek maliyet tasarrufu yapmak ve genel olarak iyi bir bulut vatandaşı olmak için kaynakları temizleyebilirsiniz:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

11. Tebrikler!

Cloud Functions, App Engine ve Cloud Run sayesinde çeşitli Web API uç noktalarını ve web ön ucunu kullanıma sunmak, kitap kitaplığını depolamak, güncellemek ve kitaplığa göz atmak için Cloud Functions, App Engine ve Cloud Run sayesinde bir dizi hizmet oluşturduk.

İşlediklerimiz

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

Daha ileri gitme

Bu somut örneği daha ayrıntılı incelemek ve genişletmek isterseniz, aşağıda araştırmak isteyebileceğiniz şeylerin bir listesi verilmiştir:

  • API Gateway'den yararlanarak veri içe aktarma işlevine ve REST API kapsayıcısına ortak bir API cephesi sunabilir, API'ye erişmek için API anahtarlarını yönetme gibi özellikler ekleyebilir veya API tüketicileri için hız sınırlamaları tanımlayabilirsiniz.
  • REST API için test oyun alanını belgelemek ve sunmak amacıyla App Engine uygulamasında Swagger-UI düğüm modülünü dağıtın.
  • Ön uçta, mevcut tarama özelliğinin ötesinde, verileri düzenlemek ve yeni kitap girişleri oluşturmak için ekstra ekranlar ekleyin. Ayrıca, Cloud Firestore veritabanını kullandığımız için değişiklikler yapıldıkça gösterilen kitap verilerini güncellemek için gerçek zamanlı özelliğinden yararlanın.