Sunucusuz Web API'leri Atölyesi

1. Genel Bakış

Bu codelab'in amacı, Google Cloud Platform tarafından sunulan "sunucusuz" hizmetlerle ilgili deneyim kazanmaktır:

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

Ayrıca, bu sunucusuz hizmetlerden yararlanarak Web ve REST API'lerini nasıl dağıtıp ölçeklendireceğinizi ve bu süreçte bazı iyi RESTful tasarım ilkelerini öğreneceksiniz.

Bu atölye çalışmasında, aşağıdakilerden oluşan bir kitaplık gezgini oluşturacağız:

  • Bir Cloud Functions işlevi: Kitaplığımızda bulunan kitapların ilk veri kümesini Cloud Firestore belge veritabanına aktarmak için
  • Veritabanımızın içeriği üzerinden bir REST API'si kullanıma sunacak bir Cloud Run container'ı,
  • 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

Yönlendirmesiz ortam kurulumu

  1. Google Cloud Console'da oturum açın ve 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. Bu bilgiyi istediğiniz zaman güncelleyebilirsiniz.
  • Proje kimliği, tüm Google Cloud projelerinde benzersizdir ve sabittir (ayarlandıktan sonra değiştirilemez). Cloud Console, benzersiz bir dizeyi otomatik olarak oluşturur. Genellikle bu dizenin ne olduğuyla ilgilenmezsiniz. Çoğu codelab'de proje kimliğinize (genellikle PROJECT_ID olarak tanımlanır) başvurmanız gerekir. Oluşturulan kimliği beğenmezseniz başka bir rastgele kimlik oluşturabilirsiniz. Dilerseniz kendi adınızı deneyerek kullanılabilir olup olmadığını kontrol edebilirsiniz. Bu adım tamamlandıktan sonra değiştirilemez ve proje süresince geçerli kalır.
  • Bazı API'lerin kullandığı üçüncü bir değer olan Proje Numarası da vardır. Bu üç değer hakkında daha fazla bilgiyi belgelerde bulabilirsiniz.
  1. Ardından, Cloud kaynaklarını/API'lerini kullanmak için Cloud Console'da faturalandırmayı etkinleştirmeniz gerekir. Bu codelab'i tamamlamak neredeyse hiç maliyetli değildir. Bu eğitimin ötesinde faturalandırılmayı önlemek için kaynakları kapatmak üzere oluşturduğunuz kaynakları veya projeyi silebilirsiniz. Yeni Google Cloud kullanıcıları 300 ABD doları değerinde ücretsiz deneme programından yararlanabilir.

Cloud Shell'i başlatma

Google Cloud, dizüstü bilgisayarınızdan uzaktan çalıştırılabilir ancak 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 Cloud Shell simgesini tıklayın:

84688aa223b1c3a2.png

Ortamın temel hazırlığı ve bağlanması yalnızca birkaç dakikanızı alır. İşlem tamamlandığında aşağıdakine benzer bir ekranla karşılaşırsınız:

320e18fedb7fbe0.png

Bu sanal makine, ihtiyaç duyacağınız tüm geliştirme araçlarını içerir. 5 GB boyutunda kalıcı bir ana dizin sunar ve Google Cloud üzerinde çalışır. Bu sayede ağ performansı ve kimlik doğrulama önemli ölçüde güçlenir. Bu codelab'deki tüm çalışmalarınızı tarayıcıda yapabilirsiniz. Herhangi bir şey yüklemeniz gerekmez.

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

Bu proje boyunca kullanacağımız çeşitli hizmetlerden yararlanmak için birkaç API'yi etkinleştireceğiz. Bu işlemi Cloud Shell'de aşağıdaki komutu çalıştırarak yapacağız:

$ 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örürsünüz:

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

Ayrıca, yol boyunca ihtiyacımız olacak bir ortam değişkeni de ayarlayacağız: işlevimizi, uygulamamızı ve kapsayıcımızı dağıtacağımız bulut bölgesi:

$ export REGION=europe-west3

Verileri Cloud Firestore veri tabanında saklayacağımız için veri tabanını oluşturmamız gerekir:

$ 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. Bu amaçla üç 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, güncellenmiş bir alan aracılığıyla koleksiyondaki sıralamayı korurken yazar veya dile göre yapacağımız aramalara karşılık gelir.

4. Kodu alın

Kodu aşağıdaki GitHub deposundan alın:

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

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

Bu laboratuvar için aşağıdaki klasör yapısına sahip olmanız gerekir:

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örde 100 kitaplık bir listenin örnek verileri bulunur.
  • 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'si sunar.
  • 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 kitaplık verileri

Veri klasöründe, muhtemelen okunmaya değer yüz kitabın listesini içeren bir books.json dosyası var. Bu JSON belgesi, JSON nesneleri içeren bir dizidir. Cloud Functions işleviyle alacağımız verilerin şekline bakalım:

[
  {
    "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 bir işlev uç noktası

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

Kodu keşfetme

package.json dosyasını inceleyerek 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 var. Cloud Functions çalışma zamanı, arka planda Express web çerçevesini de sağladığı için bunu bağımlılık olarak belirtmemiz 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) beyan ederiz. Bu açık kaynak çerçevesini, her değişiklik yaptığınızda işlevleri dağıtmak zorunda kalmadan çalıştırmak için makinenizde (bizim durumumuzda Cloud Shell'de) yerel olarak da kullanabilirsiniz. Böylece geliştirme geri bildirim döngüsünü iyileştirebilirsiniz.

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

$ npm install

start komut dosyası, işlevi yerel olarak çalıştırmak için kullanabileceğiniz bir komut sağlamak üzere Functions Framework'ü kullanır. Bu komutu çalıştırmak için aşağıdaki talimatı uygulayın:

$ npm start

İşlevle etkileşim kurmak için HTTP GET isteklerinde curl'ü veya Cloud Shell web önizlemesini kullanabilirsiniz.

Şimdi kitap verilerini içe aktarma işlevimizin mantığını içeren index.js dosyasına göz atalım:

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

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

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 bildireceğimiz işlevdir.

Sonraki birkaç talimat, aşağıdakileri kontrol etmenizi sağlar:

  • Yalnızca HTTP POST isteklerini kabul ediyoruz. Diğer HTTP yöntemlerine izin verilmediğini belirtmek için 405 durum kodunu döndürüyoruz.
  • Yalnızca application/json yükleri kabul edilir. Aksi takdirde, bunun kabul edilebilir bir yük biçimi olmadığını belirtmek için 406 durum kodu gönderilir.
    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 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ından oluşan JSON dizisini yineleyerek isbn, title, author, language, pages ve year alanlarını inceliyoruz. Kitabın ISBN kodu, birincil anahtarı veya tanımlayıcısı olarak kullanılı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"});

Verilerin büyük bir kısmı hazır olduğuna göre işlemi gerçekleştirebiliriz. Depolama işlemi başarısız olursa başarısız olduğunu bildirmek için 400 durum kodunu döndürürüz. Aksi takdirde, toplu kaydetme isteğinin kabul edildiğini belirten bir 202 durum koduyla birlikte "Tamam" yanıtı döndürebiliriz.

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

Kodu çalıştırmadan önce bağımlılıkları şu komutla yükleriz:

$ npm install

Functions Framework sayesinde işlevi yerel olarak çalıştırmak için package.json içinde tanımladığımız start komut dosyasını kullanacağız:

$ npm start

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

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

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

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

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

{"status":"OK"}

Verilerin Firestore'da depolandığı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örebiliriz.

İşlevi bulutta dağıtma

İşlevi Cloud Functions'a 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ırız. İşlevi herkese açık olarak dağıtırız (ideal olarak, bu uç noktanın güvenliğini sağlamamız gerekir). İşlevin bulunmasını istediğimiz bölgeyi belirtiriz. Ayrıca, yerel dizindeki kaynakları işaret eder ve giriş noktası olarak parseBooks (dışa aktarılan JavaScript işlevi) kullanırız.

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

c910875d4dc0aaa8.png

Dağıtım çıktısında, belirli bir adlandırma kuralına (https://${REGION}-${GOOGLE_CLOUD_PROJECT}.cloudfunctions.net/${FUNCTION_NAME}) uyan işlevinizin 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 ile komut satırından da alabilirsiniz:

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

Dağıtılan işlevimizi test etmek için yeniden kullanabilmek üzere BULK_IMPORT_URL ortam değişkeninde saklayalım.

Dağıtılan işlevi test etme

Daha önce yerel olarak çalışan işlevi test etmek için kullandığımız benzer bir curl komutuyla dağıtılan işlevi test edeceğiz. Tek değişiklik URL'de olacak:

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

Başarılı olursa aşağıdaki çıkışı döndürmesi gerekir:

{"status":"OK"}

İçe aktarma işlevimiz dağıtılıp hazır hale geldiğine ve örnek verilerimizi yüklediğimize göre artık bu veri kümesini kullanıma sunan REST API'yi geliştirebiliriz.

7. REST API sözleşmesi

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

API, aşağıdakilerden oluşan kitap JSON nesnelerini değiştirir:

  • 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 bir Integer,
  • title — kitabın başlığını içeren, boş olmayan bir String,
  • year: Kitabın yayınlandığı 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
  }

GET /books

Tüm kitapların listesini alın. Bu liste, yazar ve/veya dile göre filtrelenmiş ve her seferinde 10 sonuç olacak şekilde sayfalandırılmış olabilir.

Gövde 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.

Döndürür: Kitap nesnelerinin JSON dizisi.

Durum kodları:

  • 200: Kitap listesi getirme isteği başarıyla tamamlandığında,
  • 400 — hata oluşursa

POST /books ve POST /books/{isbn}

Yeni bir kitap yükü yayınlayın. Bu işlem, isbn yol parametresiyle (bu durumda kitap yükünde isbn kodu gerekmez) veya bu parametre olmadan (bu durumda kitap yükünde isbn kodu bulunmalıdır) yapılabilir.

Gövde yükü: kitap nesnesi.

Sorgu parametreleri: yok.

Döndürülen değer: Yok.

Durum kodları:

  • 201: Kitap başarıyla saklandığında
  • 406isbn kodu geçersizse,
  • 400 — hata oluşursa

GET /books/{isbn}

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

Gövde yükü: yok.

Sorgu parametreleri: yok.

Şunu döndürür: Kitap JSON nesnesi veya kitap yoksa hata nesnesi.

Durum kodları:

  • 200 — Kitap veritabanında bulunursa,
  • 400 (hata oluşursa)
  • 404 (kitap bulunamadıysa)
  • 406: isbn kodu geçersizse.

PUT /books/{isbn}

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

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

Sorgu parametreleri: yok.

Döndürülen değer: Güncellenen kitap.

Durum kodları:

  • 200: Kitap başarıyla güncellendiğinde
  • 400 (hata oluşursa)
  • 406: isbn kodu geçersizse.

DELETE /books/{isbn}

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

Gövde yükü: yok.

Sorgu parametreleri: yok.

Döndürülen değer: Yok.

Durum kodları:

  • 204: Kitap başarıyla silindiğinde
  • 400 — hata oluşursa

8. Bir kapsayıcıda REST API dağıtma ve kullanıma sunma

Kodu keşfetme

Dockerfile

İlk olarak, uygulama kodumuzu kapsüllemekten 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" ]

Node.JS 20 "slim" görüntüsü kullanıyoruz. /usr/src/app dizininde çalışıyoruz. Bağımlılıklarımızı tanımlayan package.json dosyasını (ayrıntılar aşağıda) kopyalıyoruz. Bağımlılıkları npm install ile yükleyip kaynak kodunu kopyalıyoruz. Son olarak, bu uygulamanın node index.js komutuyla nasıl çalıştırılması gerektiğini belirtiyoruz.

package.json

Ardından, package.json dosyasına göz atabiliriz:

{
    "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 örneğinde olduğu gibi Node.JS 14'ü kullanmak istediğimizi belirtiyoruz.

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

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

Ayrıca, uygulamayı yerel olarak başlatmak için kullanışlı olacak start komut dosyasını da belirtiyoruz. Bu komut dosyası, geliştirme ve test amaçlıdır.

index.js

Şimdi de kodun özüne, yani index.js öğesine yakından bakalım:

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

Kitap verilerimizin depolandığı books koleksiyonuna referans vererek Firestore modülünü kullanırız.

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ştirmek için kullanışlıdır. Bu, sayfalara ayırma amacıyla Link üstbilgileri oluşturduğumuzda geçerli olur (Bu konuyla ilgili daha fazla bilgiyi ilerleyen bölümlerde bulabilirsiniz).

Ardından cors modülünü yapılandırırız. Çoğu başlık genellikle kaldırıldığından, CORS üzerinden iletilmesini istediğimiz başlıkları açıkça belirtiriz. Ancak burada, normal içerik uzunluğunu ve türünü, ayrıca sayfalara ayırma için belirleyeceğ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ını ayrıştıracak ve ISBN kodları geçersizse yanıtta 406 durum koduyla yanıt verecek küçük bir hizmet işlevi geliştireceğiz.

  • GET /books

GET /books uç noktasına parça parça 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}`});
    }
});

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ıdır. Kitap listesini de 10 kitaplık parçalar halinde döndürüyoruz.

Kitaplar getirilirken bir hata oluşursa 400 durum koduyla hata döndürürüz.

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

        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'ye göre filtreleme yaptık. Bu bölümde ise kitap listesini en son güncellenme tarihine göre (en son güncellenen ilk sırada yer alacak şekilde) sıralayacağız. Ayrıca, bir sınır (döndürülecek öğe sayısı) ve bir ofset (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 ve bu sonuçları işlevin sonunda döndürülecek bir JavaScript dizisine yerleştiririz.

Bu uç nokta ile ilgili açıklamaları, iyi bir uygulamaya göz atarak tamamlayalım: Verilerin ilk, önceki, sonraki veya son sayfalarına yönelik URI bağlantılarını tanımlamak için Link başlığını kullanma (bizim durumumuzda yalnızca önceki ve sonraki sayfaları 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 yaptığımız işlem, veri sayfasının ilkinde değilsek bir önceki bağlantısı eklemektir. Veri sayfası doluysa (ör. PAGE_SIZE sabiti tarafından tanımlanan maksimum sayıda kitap içeriyorsa ve daha fazla veri içeren başka bir sayfa geleceği varsayılıyorsa) sonraki bağlantısı ekleriz. Ardından, doğru söz dizimine sahip doğru başlığı oluşturmak için Express'in resource#links() işlevini kullanırız.

Bilginize sunarız, bağlantı başlığı şu şekilde görünü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 ISBN kodunu kitap yükünde, diğeri ise yol parametresi olarak iletir. Her iki durumda da createBook() işlevimiz çağrılır:

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. Geçerli değilse işlevden döndürülür (ve 406 durum kodu ayarlanır). 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ı durumunda 201, hata durumunda ise 400 döndürülür.

Başarılı bir şekilde döndürülürken, yeni oluşturulan kaynağın bulunduğu API'nin istemcisine ipuçları vermek için konum başlığını da ayarlıyoruz. Başlık aşağıdaki gibi görünür:

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

ISBN'siyle tanımlanan bir kitabı Firestore'dan alalım.

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, bir kitabın bulunup bulunmadığını öğrenmek için kullanışlıdır. Aksi takdirde, bir hata ve 404 Bulunamadı durum kodu göndeririz. Kitap verilerini alıp döndürülecek kitabı temsil eden bir JSON nesnesi oluşturuyoruz.

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

Bu kaydı en son ne zaman güncellediğimizi hatırlamak için updated tarih/saat alanını güncelleriz. Mevcut alanları yeni değerleriyle 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 kaydedilir. Bu durumda, önceki güncellemedeki veya ilk oluşturmadaki mevcut alanlar silinir).

Ayrıca, Location üstbilgisini kitabın URI'sine yönlendirecek şekilde ayarladık.

  • DELETE /books/:isbn

Kitap silme işlemi oldukça basittir. Doküman referansında delete() yöntemini çağırırız. Herhangi bir içerik döndürmediğimiz için 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}`});
    }
});

Express / Node sunucusunu başlatma

Son olarak, varsayılan olarak 8080 bağlantı noktasını dinleyen sunucuyu başlatıyoruz:

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 önce bağımlılıkları şu komutla yükleyeceğiz:

$ npm install

Ardından şunlarla başlayabiliriz:

$ npm start

Sunucu localhost tarihinde başlatılır ve varsayılan olarak 8080 numaralı bağlantı noktasını dinler.

Aşağıdaki komutlarla Docker container oluşturmak ve container görüntüsünü çalıştırmak da mümkündür:

$ docker build -t crud-web-api .

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

Docker'da çalıştırmak, uygulamamızın kapsülleme işleminin Cloud Build ile bulutta oluşturulurken sorunsuz çalışacağını bir kez daha kontrol etmenin harika bir yoludur.

API'yi test etme

REST API kodunu nasıl çalıştırırsak çalıştıralım (doğrudan Node üzerinden veya bir Docker kapsayıcı görüntüsü aracılığıyla), artık bu koda karşı birkaç sorgu çalıştırabiliyoruz.

  • Yeni bir kitap oluşturma (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şturma (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
  • Kitap silme (oluşturduğumuz kitap):
$ 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) alma:
$ curl http://localhost:8080/books
  • Belirli bir yazarın yazdığı kitapları bulma:
$ curl http://localhost:8080/books?author=Virginia+Woolf
  • İngilizce yazılmış kitapları listele:
$ curl http://localhost:8080/books?language=English
  • Kitapların 4. sayfasını yükleme:
$ curl http://localhost:8080/books?page=3

Aramamızı hassaslaştırmak için author, language ve books sorgu parametrelerini de birleştirebiliriz.

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

REST API'nin planlandığı gibi çalıştığını görmekten mutluluk duyuyoruz. Bu nedenle, Cloud Run'da Cloud'a dağıtmak için doğru zaman geldi.

Bu işlemi iki adımda gerçekleştireceğiz:

  • Öncelikle, aşağıdaki komutla Cloud Build kullanarak kapsayıcı görüntüsünü oluşturun:
$ gcloud builds submit \
         --tag gcr.io/${GOOGLE_CLOUD_PROJECT}/crud-web-api
  • Ardından, hizmeti bu 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. Sonraki komut, container görüntüsünü kayıt defterinden dağıtır ve bulut bölgesinde dağıtır.

Cloud Console kullanıcı arayüzünde Cloud Run hizmetimizin listede göründüğünü tekrar kontrol edebiliriz:

f62fbca02a8127c0.png

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

$ 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şimde bulunacağından bir 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ırma

Bu projeye biraz ışıltı katacak son adım, REST API'mizle etkileşime girecek bir web ön ucu sağlamaktır. Bu amaçla, AJAX istekleri aracılığıyla API'yi çağıracak bazı istemci JavaScript koduyla (istemci tarafındaki Fetch API'sini kullanarak) Google App Engine'i 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 çoğu istemci tarafı JavaScript aracılığıyla tarayıcıda gerçekleşeceğinden arka uç kodu fazla değildir. Herhangi bir gelişmiş ön uç JavaScript çerçevesi kullanmayacağız. Shoelace web bileşeni kitaplığını kullanarak kullanıcı arayüzü için birkaç web bileşeniyle birlikte yalnızca "vanilya" JavaScript kullanacağız:

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

6fb9f741000a2dc1.png

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

3aa21a9e16e3244e.png

  • ve veritabanından daha fazla kitap yükleme düğmesi:

3925ad81c91bbac9.png

Tüm bu görsel bileşenler birleştirildiğinde, kitaplığımıza göz atmak için kullanılacak web sayfası aşağıdaki gibi görünür:

18a5117150977d6.png

app.yaml Yapılandırma dosyası

Bu App Engine uygulamasının kod tabanını incelemeye app.yaml yapılandırma dosyasına bakarak başlayalım. Bu, App Engine'e özgü bir dosyadır ve ortam değişkenleri, uygulamanın çeşitli "işleyicileri" gibi öğelerin yapılandırılmasına veya bazı kaynakların App Engine'in yerleşik CDN'si tarafından sunulacak statik öğeler olarak belirtilmesine olanak tanır.

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 bir Node.JS uygulaması olduğunu ve 14. sürümü kullanmak istediğimizi belirtiyoruz.

Ardından, Cloud Run hizmeti URL'mizi işaret eden bir ortam değişkeni tanımlarız. CHANGE_ME yer tutucusunu doğru URL ile güncellememiz gerekir (bunu nasıl değiştireceğinizi öğrenmek için aşağıya bakın).

Ardından ç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österir. Dördüncü satır, App Engine uygulamamızın kök URL'sinin index.html sayfasına yönlendirilmesi gerektiğini belirtir. Bu sayede, web sitesinin köküne erişirken URL'de index.html sonekini görmeyiz. Sonuncusu ise diğer tüm URL'leri (/.*) Node.JS uygulamamıza (yani, tanımladığımız statik öğelerin aksine uygulamanın "dinamik" kısmı) yönlendirecek olan varsayılan URL'dir.

Ş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

Dilerseniz CHANGE_ME dizesini app.yaml içinde doğru URL ile manuel olarak da değiştirebilirsiniz:

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 belirtmek isteriz. Kitapların ISBN kodlarını doğrulamak için Express çerçevesinin yanı sıra isbn3 NPM modülünü kullanırız.

Geliştirme bağımlılıklarında, dosya değişikliklerini izlemek için nodemon modülünü kullanacağız. npm start ile uygulamamızı yerel olarak çalıştırıp kodda bazı değişiklikler yapabilir, ^C ile uygulamayı durdurup yeniden başlatabiliriz ancak bu biraz sıkıcı bir süreçtir. Bunun yerine, değişiklikler yapıldığında 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. Herkese açık dizinin, static ara yazılımı tarafından (en azından geliştirme modunda yerel olarak çalıştırılırken) sunulabilen statik öğeler içerdiğini belirtiyoruz. Son olarak, JSON yüklerimizi ayrıştırmak için body-parser gerekir.

Tanımladığımız birkaç 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önlendirilir. Geliştirme modunda App Engine çalışma zamanında çalışmadığımız için App Engine'in URL yönlendirmesi gerçekleşmez. Bu nedenle, burada kök URL'yi HTML dosyasına yönlendiriyoruz.

Tanımladığımız ikinci uç nokta /webapi, Cloud RUN REST API'mizin URL'sini döndürür. Bu şekilde, istemci tarafı JavaScript kodu, kitap listesini almak için nereye çağrı yapacağı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}`);
});

Son olarak, Express web uygulamasını çalıştırıyoruz ve varsayılan olarak 8080 numaralı bağlantı noktasını 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, Shoelace web bileşeni kitaplığını (bir komut dosyası ve bir stil sayfası) içe aktarır.

Bir sonraki satırda, kitap ISBN kodlarının barkodlarını oluşturmak için JsBarcode kitaplığı içe aktarılıyor.

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, Shoelace bileşenlerini özel öğe etiketleriyle birlikte 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 yuva doldurma özelliğini kullanırız. Kitap listesini doldurmak için bu şablonun kopyalarını oluştururuz ve yuvalardaki 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>

Yeterince HTML inceledik. Kodu inceleme işlemi neredeyse tamamlandı. Son bir önemli bölüm kaldı: REST API'mizle etkileşimde bulunan app.js istemci taraflı JavaScript kodu.

app.js istemci tarafı JavaScript kodu

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

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

Hazır olduğunda bazı önemli sabitleri ve değişkenleri ayarlayabiliriz:

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

İlk olarak, app.yaml içinde başlangıçta 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ı olan ortam değişkeni sayesinde, REST API URL'sini ön uç kodumuza sabit kodlamamız gerekmedi.

Ayrıca, sayfalara ayırma ve dil filtrelemeyi takip etmek 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 ekliyoruz. 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çme kutusu için de benzer bir durum söz konusudur. Dil seçimindeki değişikliklerden haberdar olmak için bir etkinlik işleyici ekleriz. Düğmede olduğu gibi, REST API URL'sini, mevcut sayfayı ve dil seçimini ileterek appendMoreBooks() işlevini de çağırıyoruz.

Kitapları getirip ekleyen işlevimize 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 bu kullanıcı arayüzünde yalnızca ikisini belirtiriz:

  • page: Kitapların sayfalara ayrılması için geçerli sayfayı gösteren bir tam sayı,
  • language: Yazılı dile göre filtrelemek için kullanılan bir dil dizesi.

Ardından, kitap ayrıntılarımızı içeren JSON dizisini almak için Fetch API'yi 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';
    }

Link başlığının yanıtta bulunup bulunmamasına bağlı olarak [More books...] düğmesini gösterir veya gizleriz. Link başlığı, yüklenmesi gereken başka 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 tarafından döndürülen her kitap için, bir kitabı temsil eden bazı web bileşenleriyle şablonu klonlayacağız ve şablonun yuvalarını 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 biraz daha güzel hale getirmek için, gerçek kitapların arka kapağındaki gibi hoş bir barkod oluşturmak üzere JsBarcode kitaplığını kullanıyoruz.

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

Şimdilik bu kadar kod yeterli. Uygulamayı çalışırken görme zamanı. Öncelikle, gerçek dağıtım yapmadan önce bunu Cloud Shell'de yerel olarak yapacağız.

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

$ npm install

Uygulamayı normal şekilde çalıştırırız:

$ npm start

Alternatif olarak, nodemon sayesinde değişiklikler otomatik olarak yeniden yüklenir:

$ npm run dev

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

App Engine uygulamasını dağıtma

Uygulamamızın yerel olarak sorunsuz çalıştığından emin olduğumuza göre artık App Engine'e dağıtabiliriz.

Uygulamayı dağıtmak için aşağıdaki komutu başlatalım:

$ gcloud app deploy -q

Uygulama yaklaşık bir dakika sonra dağıtılır.

Uygulama, https://${GOOGLE_CLOUD_PROJECT}.appspot.com biçimindeki bir URL'de kullanılabilir.

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.
  • Yalnızca belirli bir dildeki kitapları görmek için o dili seçin.
  • Tüm kitapların listesine geri dönmek için seçim kutusundaki küçük çarpı işaretini kullanarak seçimi temizleyebilirsiniz.

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

Uygulamayı tutmayı düşünmüyorsanız maliyetleri düşürmek ve genel olarak iyi bir bulut kullanıcısı olmak için kaynakları temizleyebilir, bunun için de projenin tamamını silebilirsiniz:

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 bir dizi hizmet oluşturduk. Bu süreçte REST API geliştirme için bazı iyi tasarım kalıplarını kullandık.

İşlediğimiz konular

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

Daha fazla bilgi

Bu somut örneği daha ayrıntılı incelemek ve genişletmek istiyorsanız incelemek isteyebileceğiniz konuların listesini aşağıda bulabilirsiniz:

  • Veri içe aktarma işlevine ve REST API kapsayıcısına ortak bir API cephesi sağlamak, API'ye erişmek için API anahtarlarını işleme gibi özellikler eklemek veya API tüketicileri için hız sınırlamaları tanımlamak üzere API Gateway'den yararlanın.
  • REST API'yi belgelemek ve test alanı sunmak için App Engine uygulamasında Swagger-UI node 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 ek 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 üzere gerçek zamanlı özelliğinden yararlanırız.