सर्वरलेस वेब एपीआई वर्कशॉप

1. खास जानकारी

इस कोडलैब का मकसद, "सर्वर के बिना" सुविधा का अनुभव पाना है Google Cloud Platform की ओर से दी जाने वाली सेवाएं:

  • Cloud Functions — इनकी मदद से, फ़ंक्शन के तौर पर कारोबार लॉजिक की छोटी यूनिट को डिप्लॉय किया जा सकता है. ये यूनिट अलग-अलग इवेंट पर प्रतिक्रिया देती हैं (Pub/Sub मैसेज, Cloud Storage में नई फ़ाइलें, एचटीटीपी अनुरोध वगैरह),
  • App Engine — वेब ऐप्लिकेशन, वेब एपीआई, मोबाइल बैकएंड, स्टैटिक ऐसेट को डिप्लॉय करने और उपलब्ध कराने के लिए, इन टूल का इस्तेमाल तेज़ी से बढ़ाने और घटाने की सुविधाओं के साथ किया जाता है,
  • Cloud Run — कंटेनर को डिप्लॉय और स्केल करने के लिए, जिसमें कोई भी भाषा, रनटाइम या लाइब्रेरी शामिल हो सकती है.

साथ ही, वेब और REST API को डिप्लॉय और स्केल करने के लिए, बिना सर्वर वाली सेवाओं का फ़ायदा पाने का तरीका जानना. साथ ही, RESTful डिज़ाइन से जुड़ी कुछ अच्छी नीतियां देखना.

इस वर्कशॉप में, हम एक बुकशेल्फ़ एक्सप्लोरर बनाएंगे. इसमें ये चीज़ें शामिल होंगी:

  • Cloud फ़ंक्शन: हमारी लाइब्रेरी में मौजूद Cloud Firestore के दस्तावेज़ के डेटाबेस में मौजूद किताबों का शुरुआती डेटासेट इंपोर्ट करने के लिए,
  • क्लाउड रन कंटेनर: जो हमारे डेटाबेस के कॉन्टेंट पर एक REST API दिखाता है,
  • App Engine वेब फ़्रंटएंड: हमारे REST API को कॉल करके, किताबों की सूची में ब्राउज़ करने के लिए.

यहां बताया गया है कि इस कोडलैब के आखिर में वेब फ़्रंटएंड कैसा दिखेगा:

705e014da0ca5e90.png

आपको इनके बारे में जानकारी मिलेगी

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

2. सेटअप और ज़रूरी शर्तें

अपने हिसाब से एनवायरमेंट सेटअप करना

  1. Google Cloud Console में साइन इन करें और नया प्रोजेक्ट बनाएं या किसी मौजूदा प्रोजेक्ट का फिर से इस्तेमाल करें. अगर आपके पास पहले से Gmail या Google Workspace खाता नहीं है, तो आपको नया खाता बनाना होगा.

295004821बाबा6a87.png

37d264871000675d.png

96d86d3d5655cdbe.png

  • प्रोजेक्ट का नाम, इस प्रोजेक्ट में हिस्सा लेने वाले लोगों का डिसप्ले नेम होता है. यह एक वर्ण स्ट्रिंग है, जिसका इस्तेमाल Google API नहीं करता. इसे कभी भी अपडेट किया जा सकता है.
  • प्रोजेक्ट आईडी, Google Cloud के सभी प्रोजेक्ट के लिए यूनीक होता है. साथ ही, इसे बदला नहीं जा सकता. इसे सेट करने के बाद बदला नहीं जा सकता. Cloud Console, एक यूनीक स्ट्रिंग अपने-आप जनरेट करता है; आम तौर पर, आपको उसके होने की कोई परवाह नहीं होती. ज़्यादातर कोडलैब में, आपको अपना प्रोजेक्ट आईडी बताना होगा. आम तौर पर, इसकी पहचान PROJECT_ID के रूप में की जाती है. अगर आपको जनरेट किया गया आईडी पसंद नहीं है, तो किसी भी क्रम में एक और आईडी जनरेट किया जा सकता है. दूसरा तरीका यह है कि आप खुद भी आज़माकर देखें कि वह उपलब्ध है या नहीं. इस चरण के बाद, इसे बदला नहीं जा सकता. साथ ही, यह प्रोजेक्ट के खत्म होने तक बना रहता है.
  • आपकी जानकारी के लिए, प्रोजेक्ट नंबर नाम की एक तीसरी वैल्यू दी गई है. इसका इस्तेमाल कुछ एपीआई करते हैं. दस्तावेज़ में इन तीनों वैल्यू के बारे में ज़्यादा जानें.
  1. इसके बाद, आपको क्लाउड संसाधनों/एपीआई का इस्तेमाल करने के लिए, Cloud Console में बिलिंग चालू करनी होगी. इस कोडलैब का इस्तेमाल करने पर, आपको ज़्यादा पैसे नहीं चुकाने होंगे. इस ट्यूटोरियल के अलावा, बिलिंग से बचने के लिए संसाधनों को बंद करें. इसके लिए, अपने बनाए गए संसाधनों को मिटाएं या प्रोजेक्ट को मिटाएं. Google Cloud के नए उपयोगकर्ता, 300 डॉलर के मुफ़्त ट्रायल वाले प्रोग्राम में हिस्सा ले सकते हैं.

Cloud Shell शुरू करना

Google Cloud को आपके लैपटॉप से, कहीं से भी ऑपरेट किया जा सकता है. हालांकि, इस कोडलैब में Google Cloud Shell का इस्तेमाल किया जा रहा है. यह क्लाउड में चलने वाला कमांड लाइन एनवायरमेंट है.

Google Cloud Console में जाकर, सबसे ऊपर दाईं ओर मौजूद टूलबार पर क्लाउड शेल आइकॉन पर क्लिक करें:

84688aa223b1c3a2.png

प्रावधान करने और एनवायरमेंट से कनेक्ट होने में कुछ ही समय लगेगा. उसके पूरा हो जाने पर, आपको कुछ ऐसा दिखाई देगा:

320e18fedb7fbe0.png

इस वर्चुअल मशीन में ऐसे सभी डेवलपमेंट टूल मौजूद हैं जिनकी आपको ज़रूरत पड़ेगी. यह पांच जीबी की स्थायी होम डायरेक्ट्री उपलब्ध कराता है और Google Cloud पर चलता है. यह नेटवर्क की परफ़ॉर्मेंस और पुष्टि करने की प्रक्रिया को बेहतर बनाता है. इस कोडलैब (कोड बनाना सीखना) में आपका सारा काम ब्राउज़र में किया जा सकता है. आपको कुछ भी इंस्टॉल करने की ज़रूरत नहीं है.

3. एनवायरमेंट को तैयार करें और Cloud API चालू करें

इस पूरे प्रोजेक्ट में हमें जिन सेवाओं की ज़रूरत होगी उनका इस्तेमाल करने के लिए, हम कुछ एपीआई चालू करेंगे. ऐसा करने के लिए, हम Cloud Shell में यह कमांड लॉन्च करेंगे:

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

कुछ समय बाद, आपको बदलाव दिखेगा कि कार्रवाई पूरी हो गई है:

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

इस दौरान, हम एक ऐसा एनवायरमेंट वैरिएबल भी सेट अप करेंगे जिसकी ज़रूरत हमें पड़ेगी: वह क्लाउड क्षेत्र जहां हम अपने फ़ंक्शन, ऐप्लिकेशन, और कंटेनर को डिप्लॉय करेंगे:

$ export REGION=europe-west3

हम Cloud Firestore डेटाबेस में डेटा सेव करेंगे, इसलिए हमें डेटाबेस बनाना होगा:

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

बाद में, इस कोडलैब में, REST API लागू करते समय, हमें डेटा को क्रम से लगाना होगा और उसे फ़िल्टर करना होगा. इस काम के लिए, हम तीन इंडेक्स बनाएंगे:

$ 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 

ये तीनों इंडेक्स, उन खोजों पर आधारित होते हैं जो हम अपडेट किए गए फ़ील्ड के ज़रिए, कलेक्शन में क्रम को बनाए रखते हुए, लेखक या भाषा के हिसाब से करते हैं.

4. कोड प्राप्त करें

नीचे दिए गए GitHub रिपॉज़िटरी से कोड पाएं:

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

ऐप्लिकेशन कोड Node.JS का इस्तेमाल करके लिखा जाता है.

आपके पास नीचे दिया गया वह फ़ोल्डर स्ट्रक्चर होगा जो इस लैब के लिए सही है:

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

ये काम के फ़ोल्डर हैं:

  • data — इस फ़ोल्डर में 100 किताबों की सूची का एक सैंपल डेटा होता है.
  • function-import — यह फ़ंक्शन सैंपल डेटा इंपोर्ट करने के लिए, एंडपॉइंट की सुविधा देगा.
  • run-crud — यह कंटेनर, Cloud Firestore में स्टोर किए गए किताब के डेटा को ऐक्सेस करने के लिए, Web API को दिखाएगा.
  • appengine-frontend — यह App Engine वेब ऐप्लिकेशन किताबों की सूची ब्राउज़ करने के लिए एक आसान रीड-ओनली फ़्रंटएंड दिखाएगा.

5. सैंपल बुक की लाइब्रेरी का डेटा

डेटा फ़ोल्डर में, हमारे पास एक books.json फ़ाइल है, जिसमें सौ किताबों की सूची है. यह शायद पढ़ने लायक है. यह JSON दस्तावेज़, JSON ऑब्जेक्ट का कलेक्शन है. आइए, डेटा के उस आकार पर एक नज़र डालते हैं जिसे हम Cloud Function के ज़रिए इंजेस्ट करेंगे:

[
  {
    "isbn": "9780435272463",
    "author": "Chinua Achebe",
    "language": "English",
    "pages": 209,
    "title": "Things Fall Apart",
    "year": 1958
  },
  {
    "isbn": "9781414251196",
    "author": "Hans Christian Andersen",
    "language": "Danish",
    "pages": 784,
    "title": "Fairy tales",
    "year": 1836
  },
  ...
]

इस कलेक्शन में हमारी किताबों की सभी एंट्री में नीचे दी गई जानकारी शामिल है:

  • isbn — किताब की पहचान करने वाला ISBN-13 कोड.
  • author — इस किताब के लेखक का नाम.
  • language — बोली जाने वाली वह भाषा जिसमें किताब लिखी गई है.
  • pages — किताब में मौजूद पेजों की संख्या.
  • title — किताब का नाम.
  • year — किताब पब्लिश होने का साल.

6. किताब के सैंपल का डेटा इंपोर्ट करने के लिए फ़ंक्शन एंडपॉइंट

इस पहले सेक्शन में, हम उस एंडपॉइंट को लागू करेंगे जिसका इस्तेमाल किताब के सैंपल का डेटा इंपोर्ट करने के लिए किया जाएगा. इस काम के लिए हम Cloud Functions का इस्तेमाल करेंगे.

कोड को एक्सप्लोर करना

आइए, package.json फ़ाइल पर नज़र डालकर शुरुआत करें:

{
    "name": "function-import",
    "description": "Import sample book data",
    "license": "Apache-2.0",
    "dependencies": {
        "@google-cloud/firestore": "^4.9.9"
    },
    "devDependencies": {
        "@google-cloud/functions-framework": "^3.1.0"
    },
    "scripts": {
        "start": "npx @google-cloud/functions-framework --target=parseBooks"
    }
}

रनटाइम डिपेंडेंसी में डेटाबेस को ऐक्सेस करने और अपनी किताब के डेटा को स्टोर करने के लिए, हमें सिर्फ़ @google-cloud/firestore एनपीएम मॉड्यूल की ज़रूरत होती है. हुड के तहत, Cloud Functions रनटाइम भी एक्सप्रेस वेब फ़्रेमवर्क उपलब्ध कराता है, इसलिए हमें इसे डिपेंडेंसी के तौर पर एलान करने की ज़रूरत नहीं है.

डेवलपमेंट डिपेंडेंसी में, हम फ़ंक्शन फ़्रेमवर्क (@google-cloud/functions-framework) का एलान करते हैं. यह आपके फ़ंक्शन शुरू करने के लिए इस्तेमाल किया जाने वाला रनटाइम फ़्रेमवर्क है. यह एक ओपन सोर्स फ़्रेमवर्क है. इसे अपनी मशीन पर (हमारे मामले में, Cloud Shell के अंदर) स्थानीय तौर पर भी इस्तेमाल किया जा सकता है. इससे हर बार कोई बदलाव करने पर, डिप्लॉय किए बिना फ़ंक्शन चलाए जा सकते हैं. इससे डेवलपमेंट फ़ीडबैक लूप में सुधार होता है.

डिपेंडेंसी इंस्टॉल करने के लिए, install कमांड का इस्तेमाल करें:

$ npm install

start स्क्रिप्ट आपको एक निर्देश देने के लिए फ़ंक्शन फ़्रेमवर्क का इस्तेमाल करती है, जिसका इस्तेमाल आप नीचे दिए गए निर्देश के साथ स्थानीय तौर पर फ़ंक्शन चलाने के लिए कर सकते हैं:

$ npm start

फ़ंक्शन के साथ इंटरैक्ट करने के लिए, एचटीटीपी जीईटी अनुरोधों के लिए कर्ल या क्लाउड शेल वेब प्रीव्यू का इस्तेमाल किया जा सकता है.

आइए, अब उस index.js फ़ाइल पर नज़र डालते हैं, जिसमें हमारे किताब के डेटा इंपोर्ट फ़ंक्शन का लॉजिक मौजूद है:

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

हम Firestore मॉड्यूल को इंस्टैंशिएट करते हैं और किताबों के कलेक्शन की तरफ़ पॉइंट करते हैं (रिलेशनल डेटाबेस में दी गई टेबल की तरह).

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 फ़ंक्शन एक्सपोर्ट कर रहे हैं. इसे बाद में डिप्लॉय करने पर, हम इस फ़ंक्शन के बारे में बताएंगे.

अगले कुछ निर्देशों में इसकी जांच की जा रही है:

  • हम सिर्फ़ एचटीटीपी POST अनुरोध स्वीकार कर रहे हैं. इसके अलावा, हम 405 स्टेटस कोड दिखाकर यह बताते हैं कि एचटीटीपी के अन्य तरीकों की अनुमति नहीं है.
  • हम सिर्फ़ application/json पेलोड स्वीकार कर रहे हैं. इसके अलावा, हम 406 स्टेटस कोड भेजकर यह बताते हैं कि यह पेलोड फ़ॉर्मैट स्वीकार नहीं किया जा सकता.
    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()
        });
    }

इसके बाद, हम अनुरोध के body के ज़रिए JSON पेलोड को वापस ला सकते हैं. हम सभी किताबों को एक साथ स्टोर करने के लिए, एक Firestore बैच ऑपरेशन तैयार कर रहे हैं. हम किताब की जानकारी वाले JSON कलेक्शन को फिर से दोहराते हैं. इसमें isbn, title, author, language, pages, और year फ़ील्ड शामिल होते हैं. किताब का ISBN कोड, मुख्य कुंजी या आइडेंटिफ़ायर के तौर पर इस्तेमाल किया जाएगा.

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

अब जब कि ज़्यादा डेटा तैयार है, हम कार्रवाई कर सकते हैं. अगर स्टोरेज काम नहीं करता है, तो हम 400 स्टेटस कोड दिखाते हैं, ताकि यह पता चल सके कि स्टोरेज काम नहीं कर रहा है. ऐसा न करने पर, हम 202 स्टेटस कोड के साथ ठीक जवाब दे सकते हैं. इससे पता चलेगा कि बल्क सेव करने का अनुरोध स्वीकार किया गया है.

इंपोर्ट फ़ंक्शन को चलाना और उसकी जांच करना

कोड चलाने से पहले, हम डिपेंडेंसी को इनके साथ इंस्टॉल करेंगे:

$ npm install

फ़ंक्शन को स्थानीय तौर पर चलाने के लिए, फ़ंक्शन फ़्रेमवर्क का धन्यवाद, हम package.json में बताए गए start स्क्रिप्ट निर्देश का इस्तेमाल करेंगे:

$ npm start

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

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

अपने लोकल फ़ंक्शन में एचटीटीपी POST का अनुरोध भेजने के लिए, यह तरीका अपनाया जा सकता है:

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

इस निर्देश को लॉन्च करते समय, आपको यह आउटपुट दिखेगा, जिससे यह पुष्टि होगी कि फ़ंक्शन स्थानीय तौर पर चल रहा है:

{"status":"OK"}

Cloud Console के यूज़र इंटरफ़ेस (यूआई) पर जाकर भी यह देखा जा सकता है कि डेटा वाकई में मौजूद है या नहीं:

409982568cebdbf8.png

ऊपर दिए गए स्क्रीनशॉट में, हम बनाया गया books संग्रह, किताब के ISBN कोड से पहचाने गए किताब के दस्तावेज़ों की सूची और दाईं ओर उस खास किताब की एंट्री का विवरण देख सकते हैं.

क्लाउड में फ़ंक्शन को डिप्लॉय करना

फ़ंक्शन को Cloud Functions में डिप्लॉय करने के लिए, हम function-import डायरेक्ट्री में इस कमांड का इस्तेमाल करेंगे:

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

हम फ़ंक्शन को bulk-import के सिम्बॉलिक नाम के साथ डिप्लॉय करते हैं. यह फ़ंक्शन एचटीटीपी अनुरोधों से ट्रिगर होता है. हम Node.JS 20 रनटाइम का इस्तेमाल करते हैं. हम फ़ंक्शन को सार्वजनिक तौर पर डिप्लॉय करते हैं. आम तौर पर, हमें उस एंडपॉइंट को सुरक्षित करना चाहिए. हम उस क्षेत्र को बताते हैं जहां हम फ़ंक्शन को रखना चाहते हैं. साथ ही, हम लोकल डायरेक्ट्री में मौजूद सोर्स की ओर इशारा करते हैं और एंट्री पॉइंट के तौर पर parseBooks (एक्सपोर्ट किया गया JavaScript फ़ंक्शन) का इस्तेमाल करते हैं.

कुछ मिनट या इससे कम समय के बाद, फ़ंक्शन को क्लाउड पर डिप्लॉय कर दिया जाता है. Cloud Console के यूज़र इंटरफ़ेस (यूआई) में, आपको यह फ़ंक्शन दिखेगा:

c910875d4dc0aaa8.png

डिप्लॉयमेंट आउटपुट में, आपको अपने फ़ंक्शन का यूआरएल दिखेगा, जो नाम रखने के एक खास तरीके (https://${REGION}-${GOOGLE_CLOUD_PROJECT}.cloudfunctions.net/${FUNCTION_NAME}) का पालन करता है. साथ ही, आपको यह एचटीटीपी ट्रिगर यूआरएल, ट्रिगर टैब में Cloud Console के यूज़र इंटरफ़ेस (यूआई) में भी मिल सकता है:

380ffc46eb56441e.png

gcloud की मदद से, कमांड-लाइन के ज़रिए भी यूआरएल को वापस पाया जा सकता है:

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

आइए, इसे BULK_IMPORT_URL एनवायरमेंट वैरिएबल में सेव करें, ताकि हम डिप्लॉय किए गए फ़ंक्शन की जांच करने के लिए इसका फिर से इस्तेमाल कर सकें.

डिप्लॉय किए गए फ़ंक्शन की जांच करना

इसी तरह के कर्ल निर्देश की मदद से, हम डिप्लॉय किए गए फ़ंक्शन की जांच करेंगे. इस निर्देश का इस्तेमाल, पहले स्थानीय तौर पर चल रहे फ़ंक्शन की जांच करने के लिए किया जाता था. सिर्फ़ यूआरएल में ही बदलाव होगा:

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

दोबारा काम करने पर, यह नीचे दिया गया आउटपुट दिखाएगा:

{"status":"OK"}

अब हमारा इंपोर्ट फ़ंक्शन डिप्लॉय हो गया है और तैयार है. साथ ही, हमने अपना सैंपल डेटा अपलोड कर दिया है. इसलिए, अब हम इस डेटासेट के बारे में बताने वाला REST API डेवलप करते हैं.

7. REST API का कॉन्ट्रैक्ट

हालांकि, हम ओपन एपीआई स्पेसिफ़िकेशन जैसी जानकारी का इस्तेमाल करके, एपीआई समझौते के बारे में नहीं बता रहे हैं. हम REST API के अलग-अलग एंडपॉइंट पर नज़र डालते हैं.

एपीआई, किताब के JSON ऑब्जेक्ट को एक्सचेंज करता है, जिनमें ये शामिल हैं:

  • isbn (ज़रूरी नहीं) — 13 वर्णों का String, जो मान्य ISBN कोड दिखाता है,
  • author — एक खाली String है, जो किताब के लेखक के नाम के बारे में बताता है,
  • language — एक खाली String नहीं होता है. इसमें वह भाषा होती है जिसमें किताब लिखी गई थी,
  • pages — किताब के पेजों की संख्या के लिए पॉज़िटिव Integer,
  • title — किताब के नाम के साथ एक String खाली नहीं है,
  • year — किताब के पब्लिश होने के साल के लिए, यह Integer वैल्यू होती है.

किताब के पेलोड का उदाहरण:

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

/books पाना

सभी किताबों की सूची पाएं. यह सूची, लेखक और/या भाषा के हिसाब से फ़िल्टर की गई होती है और एक साथ 10 नतीजों वाली विंडो के हिसाब से होती है.

बॉडी पेलोड: कोई नहीं.

क्वेरी पैरामीटर:

  • author (ज़रूरी नहीं) — लेखक के हिसाब से, किताबों की सूची को फ़िल्टर करता है,
  • language (ज़रूरी नहीं) — किताबों की सूची को भाषा के हिसाब से फ़िल्टर करता है,
  • page (ज़रूरी नहीं, डिफ़ॉल्ट = 0) — यह बताता है कि नतीजों के पेज की रैंक क्या है.

यह फ़ंक्शन दिखाता है: किताब के ऑब्जेक्ट का JSON अरे.

स्थिति कोड:

  • 200 — किताबों की सूची को फ़ेच करने का अनुरोध पूरा होने पर,
  • 400 — अगर कोई गड़बड़ी होती है.

POST /books और POST /books/{isbn}

isbn पाथ पैरामीटर के साथ (इस मामले में, किताब के पेलोड में isbn कोड की ज़रूरत नहीं है) या बिना (इस स्थिति में, किताब के पेलोड में isbn कोड मौजूद होना चाहिए) के साथ नया किताब पेलोड पोस्ट करें

बॉडी पेलोड: किताब से जुड़ा ऑब्जेक्ट.

क्वेरी पैरामीटर: कोई नहीं.

रिटर्न: कुछ नहीं.

स्थिति कोड:

  • 201 — किताब सेव होने के बाद,
  • 406 — अगर isbn कोड अमान्य है, तो
  • 400 — अगर कोई गड़बड़ी होती है.

पाएं /books/{isbn}

लाइब्रेरी से किताब हासिल करता है, जिसकी पहचान उसके isbn कोड से की जाती है, जिसे पाथ पैरामीटर के तौर पर पास किया जाता है.

बॉडी पेलोड: कोई नहीं.

क्वेरी पैरामीटर: कोई नहीं.

वापस करता है: किताब का JSON ऑब्जेक्ट या किताब मौजूद न होने पर गड़बड़ी वाला कोई ऑब्जेक्ट.

स्थिति कोड:

  • 200 — अगर किताब, डेटाबेस में मौजूद है,
  • 400 — कोई गड़बड़ी होने पर,
  • 404 — अगर किताब नहीं मिलती, तो
  • 406 — अगर isbn कोड अमान्य है.

पुट /books/{isbn}

पाथ पैरामीटर के तौर पर पास की गई मौजूदा किताब को अपडेट करता है. इसकी पहचान, isbn से की जाती है.

बॉडी पेलोड: किताब से जुड़ा ऑब्जेक्ट. सिर्फ़ उन फ़ील्ड को पास किया जा सकता है जिन्हें अपडेट करने की ज़रूरत है. अन्य फ़ील्ड वैकल्पिक होते हैं.

क्वेरी पैरामीटर: कोई नहीं.

वापस दिखाता है: अपडेट की गई किताब.

स्थिति कोड:

  • 200 — किताब अपडेट होने के बाद,
  • 400 — कोई गड़बड़ी होने पर,
  • 406 — अगर isbn कोड अमान्य है.

मिटाएं /books/{isbn}

पाथ पैरामीटर के तौर पर पास की गई मौजूदा किताब को मिटाता है. इसकी पहचान, isbn से की जाती है.

बॉडी पेलोड: कोई नहीं.

क्वेरी पैरामीटर: कोई नहीं.

रिटर्न: कुछ नहीं.

स्थिति कोड:

  • 204 — किताब मिटाने के बाद,
  • 400 — अगर कोई गड़बड़ी होती है.

8. किसी कंटेनर में REST API को डिप्लॉय और दिखाना

कोड को एक्सप्लोर करना

डॉकरफ़ाइल

आइए Dockerfile पर नज़र डालते हैं, जो हमारे ऐप्लिकेशन कोड को कंटेनर बनाने के लिए ज़िम्मेदार होगा:

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" इमेज का इस्तेमाल कर रहे हैं. हम /usr/src/app डायरेक्ट्री में काम कर रहे हैं. हम दूसरी चीज़ों के साथ-साथ, हमारी डिपेंडेंसी के बारे में बताने वाली package.json फ़ाइल को कॉपी कर रहे हैं. इसकी जानकारी नीचे दी गई है. हम npm install की मदद से डिपेंडेंसी इंस्टॉल करते हैं और सोर्स कोड को कॉपी करते हैं. अंत में, हम node index.js आदेश से बताते हैं कि इस ऐप्लिकेशन को कैसे चलाना चाहिए.

package.json

आगे, हम package.json फ़ाइल पर एक नज़र डाल सकते हैं:

{
    "name": "run-crud",
    "description": "CRUD operations over book data",
    "license": "Apache-2.0",
    "engines": {
        "node": ">= 20.0.0"
    },
    "dependencies": {
        "@google-cloud/firestore": "^4.9.9",
        "cors": "^2.8.5",
        "express": "^4.17.1",
        "isbn3": "^1.1.10"
    },
    "scripts": {
        "start": "node index.js"
    }
}

साथ ही, हम यह भी बताना चाहते हैं कि हम Node.JS 14 को Dockerfile की तरह ही इस्तेमाल करना चाहते हैं.

हमारा वेब एपीआई ऐप्लिकेशन इन बातों पर निर्भर करता है:

  • डेटाबेस में किताब के डेटा को ऐक्सेस करने के लिए Firestore एनपीएम मॉड्यूल,
  • CORS (क्रॉस ऑरिजिन रिसॉर्स शेयरिंग) अनुरोधों को मैनेज करने के लिए, cors लाइब्रेरी का इस्तेमाल किया जाता है. इसकी वजह यह है कि हमारे REST API को हमारे App Engine वेब ऐप्लिकेशन फ़्रंटएंड के क्लाइंट कोड से शुरू किया जाएगा,
  • एक्सप्रेस फ़्रेमवर्क, हमारे एपीआई को डिज़ाइन करने के लिए हमारा वेब फ़्रेमवर्क होगा.
  • इसके बाद, isbn3 मॉड्यूल की मदद से किताब के ISBN कोड की पुष्टि की जा सकती है.

हमने start स्क्रिप्ट भी तय की है, जो डेवलपमेंट और टेस्टिंग के लिए, ऐप्लिकेशन को स्थानीय तौर पर शुरू करने में इस्तेमाल होगी.

index.js

आइए, index.js के बारे में गहराई से जानते हैं और कोड के मुख्य फ़ंक्शन के बारे में जानते हैं:

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

हमें Firestore मॉड्यूल की ज़रूरत है और हम books कलेक्शन का रेफ़रंस देते हैं, जहां हमारी किताबों का डेटा स्टोर है.

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 को लागू करने के लिए, वेब फ़्रेमवर्क के तौर पर Express का इस्तेमाल कर रहे हैं. हम अपने एपीआई के साथ एक्सचेंज किए गए JSON पेलोड को पार्स करने के लिए, body-parser मॉड्यूल का इस्तेमाल कर रहे हैं.

यूआरएल में हेर-फेर करने में querystring मॉड्यूल मददगार होता है. ऐसा तब होगा, जब हम पेज नंबर डालने के मकसद से Link हेडर बनाएंगे (इस बारे में ज़्यादा जानकारी बाद में दी जाएगी).

इसके बाद, हम cors मॉड्यूल को कॉन्फ़िगर करते हैं. हम साफ़ तौर पर उन हेडर के बारे में बताते हैं जिन्हें हम सीओआरएस के ज़रिए पास करना चाहते हैं. ज़्यादातर हेडर को हटा दिया जाता है. हालांकि, हम कॉन्टेंट की सामान्य लंबाई और टाइप के साथ-साथ Link हेडर को भी रखना चाहते हैं, जिसे हम पेज पर नंबर डालने के लिए तय करेंगे.

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 कोड को पार्स करने और उनकी पुष्टि करने के लिए, isbn3 एनपीएम मॉड्यूल का इस्तेमाल करेंगे. साथ ही, हम एक छोटा यूटिलिटी फ़ंक्शन डेवलप करेंगे, जो ISBN कोड को पार्स करेगा और रिस्पॉन्स पर, 406 स्टेटस कोड के साथ जवाब देगा. अगर ISBN कोड अमान्य हैं, तो हम उसका इस्तेमाल करेंगे.

  • GET /books

आइए, GET /books एंडपॉइंट पर एक नज़र डालते हैं. इसे अलग-अलग हिस्सों में बांटा गया है:

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

हम डेटाबेस से जुड़ी क्वेरी करने की तैयारी कर रहे हैं. इसके लिए हम एक क्वेरी तैयार कर रहे हैं. यह क्वेरी, लेखक और/या भाषा के हिसाब से फ़िल्टर करने के लिए, वैकल्पिक क्वेरी पैरामीटर पर निर्भर करेगी. हम 10 किताबों के अलग-अलग हिस्सों की किताबों की सूची भी दे रहे हैं.

अगर किताबों को फ़ेच करते समय कोई गड़बड़ी होती है, तो हम 400 स्टेटस कोड वाली गड़बड़ी दिखाते हैं.

चलिए, उस एंडपॉइंट के काटे गए हिस्से पर ज़ूम इन करते हैं:

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

पिछले सेक्शन में, हमने author और language के हिसाब से फ़िल्टर किया था. हालांकि, इस सेक्शन में, हम किताबों की सूची को आखिरी बार अपडेट किए जाने की तारीख के हिसाब से क्रम में लगा रहे थे. पिछली बार अपडेट किए जाने की तारीख पहले आती है. और हम एक सीमा (लौटने वाले तत्वों की संख्या), और एक ऑफ़सेट (वह शुरुआती बिंदु जहां से किताबों का अगला बैच देना है) तय करके, नतीजे को पेजों में भी जोड़ेंगे.

हम क्वेरी पर काम करते हैं, डेटा का स्नैपशॉट हासिल करते हैं, और उन नतीजों को JavaScript कलेक्शन में रखते हैं. इन्हें फ़ंक्शन के आखिर में दिखाया जाता है.

चलिए, एक सही तरीका अपनाकर इस एंडपॉइंट के बारे में पूरी जानकारी देते हैं: डेटा के पहले, पिछले, अगले या आखिरी पेज के यूआरआई लिंक तय करने के लिए, Link हेडर का इस्तेमाल करें (हमारे मामले में, हम सिर्फ़ पिछले और अगले पेज के बारे में बताएंगे).

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

पहली बार में यह तर्क थोड़ा जटिल लग सकता है, लेकिन अगर हम डेटा के पहले पेज पर नहीं हैं, तो हम पिछला लिंक जोड़ना चाहते हैं. और अगर डेटा का पेज भरा हुआ है, तो हम अगला लिंक जोड़ते हैं (यानी इसमें PAGE_SIZE कॉन्सटेंट के मुताबिक तय की गई ज़्यादा से ज़्यादा संख्या में किताबें शामिल हैं). ऐसा यह मानते हुए किया जाता है कि एक और लिंक मिलने वाला है, जिससे ज़्यादा डेटा मिल रहा है. इसके बाद, हम सही सिंटैक्स के साथ सही हेडर बनाने के लिए, Express के resource#links() फ़ंक्शन का इस्तेमाल करते हैं.

आपकी जानकारी के लिए, लिंक हेडर कुछ इस तरह दिखेगा:

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

नई किताब बनाने के लिए, यहां दोनों एंडपॉइंट मौजूद हैं. एक किताब के पेलोड में ISBN कोड पास करता है, जबकि दूसरा उसे पाथ पैरामीटर के तौर पर पास करता है. दोनों ही स्थितियों में, हमारे createBook() फ़ंक्शन को कॉल करें:

async function createBook(isbn, req, res) {
    const parsedIsbn = isbnOK(isbn, res);
    if (!parsedIsbn) return;

    const {title, author, pages, year, language} = req.body;

    try {
        const docRef = bookStore.doc(parsedIsbn.isbn13);
        await docRef.set({
            title, author, pages, year, language,
            updated: Firestore.Timestamp.now()
        });
        console.log(`Saved book ${parsedIsbn.isbn13}`);

        res.status(201)
            .location(`/books/${parsedIsbn.isbn13}`)
            .send({status: `Book ${parsedIsbn.isbn13} created`});
    } catch (e) {
        console.error(`Failed to save book ${parsedIsbn.isbn13}`, e);
        res.status(400)
            .send({error: `Impossible to create book ${parsedIsbn.isbn13}: ${e.message}`});
    }    
}

हम जांच करते हैं कि isbn कोड मान्य है या नहीं. अगर ऐसा नहीं है, तो फ़ंक्शन से वापस लौटें (और 406 स्टेटस कोड सेट करें). हम अनुरोध के मुख्य भाग में पास किए गए पेलोड से किताब के फ़ील्ड को वापस लाते हैं. इसके बाद, हम किताब की जानकारी Firestore में सेव करेंगे. सफल होने पर 201 और फ़ेल होने पर 400.

वापस आने पर, हम लोकेशन हेडर भी सेट करते हैं, ताकि एपीआई के क्लाइंट को इस बात का संकेत दिया जा सके कि नया रिसॉर्स कहां मौजूद है. हेडर ऐसा दिखेगा:

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

आइए, Firestore से एक ऐसी किताब फ़ेच करते हैं, जिसकी पहचान उसके ISBN के ज़रिए की गई है.

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

हमेशा की तरह, हम जांच करते हैं कि ISBN मान्य है या नहीं. हम किताब को वापस पाने के लिए, Firestore से एक क्वेरी करते हैं. snapshot.exists प्रॉपर्टी से यह जानने में मदद मिलती है कि असल में कोई किताब मिली है या नहीं. अगर ऐसा नहीं होता है, तो हम गड़बड़ी और 404 नहीं मिला स्टेटस कोड वापस भेजते हैं. हम किताब का डेटा वापस लाते हैं और किताब के बारे में जानकारी देने वाला एक JSON ऑब्जेक्ट बनाते हैं, जिसे दिखाया जाता है.

  • PUT /books/:isbn

हम एक मौजूदा किताब को अपडेट करने के लिए, PUT तरीके का इस्तेमाल कर रहे हैं.

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 की तारीख/समय वाले फ़ील्ड को अपडेट करते हैं, ताकि यह याद रखा जा सके कि हमने उस रिकॉर्ड को पिछली बार कब अपडेट किया था. हम {merge:true} रणनीति का इस्तेमाल करते हैं. यह मौजूदा फ़ील्ड को उनकी नई वैल्यू से बदल देती है. ऐसा नहीं करने पर, सभी फ़ील्ड हटा दिए जाते हैं और पेलोड में सिर्फ़ नए फ़ील्ड सेव किए जाते हैं. साथ ही, पिछले अपडेट या शुरुआती क्रिएशन से मौजूदा फ़ील्ड हमेशा के लिए मिट जाते हैं.

हम Location हेडर को किताब के यूआरआई की जानकारी देने के लिए भी सेट करते हैं.

  • DELETE /books/:isbn

किताबों को मिटाना बहुत आसान है. हम सिर्फ़ दस्तावेज़ के रेफ़रंस में, delete() तरीके को कॉल करते हैं. हम 204 स्टेटस कोड दिखाते हैं, क्योंकि हम कोई कॉन्टेंट नहीं दिखाते.

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

एक्सप्रेस / नोड सर्वर शुरू करना

आखिरी लेकिन अहम बात, हम डिफ़ॉल्ट रूप से 8080 पोर्ट पर सुनते हुए सर्वर शुरू करते हैं:

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

ऐप्लिकेशन को स्थानीय तौर पर चलाना

ऐप्लिकेशन को स्थानीय तौर पर चलाने के लिए, हम पहले डिपेंडेंसी को इनके साथ इंस्टॉल करेंगे:

$ npm install

इसके बाद, हम इन चीज़ों से शुरुआत कर सकते हैं:

$ npm start

सर्वर localhost से शुरू होगा और डिफ़ॉल्ट रूप से पोर्ट 8080 पर सुनें.

Docker कंटेनर बनाने के साथ-साथ, कंटेनर इमेज को भी यहां दिए गए निर्देशों का पालन करके चलाया जा सकता है:

$ docker build -t crud-web-api .

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

Docker के अंदर काम करना, यह दोबारा जांचने का एक शानदार तरीका है कि हमारे ऐप्लिकेशन का कंटेनराइज़ेशन ठीक से काम करेगा, क्योंकि हम इसे Cloud Build के साथ क्लाउड में बनाते हैं.

एपीआई की जांच करना

हम REST API कोड को (सीधे नोड या Docker कंटेनर इमेज के ज़रिए) से कैसे चलाते हैं, अब हम उसके लिए कुछ क्वेरी चला सकते हैं.

  • नई किताब बनाएं (मुख्य हिस्से में 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
  • नई किताब बनाएं (पाथ पैरामीटर में ISBN):
$ curl -XPOST -d '{"title":"Book","author":"me","pages":123,"year":2021,"language":"French"}' \
    -H "Content-Type: application/json" \
    http://localhost:8080/books/9782070368228
  • किताब (जिसे हमने बनाया है) मिटाएं:
$ curl -XDELETE http://localhost:8080/books/9782070368228
  • ISBN से किसी किताब को वापस पाना:
$ curl http://localhost:8080/books/9780140449136
$ curl http://localhost:8080/books/9782070360536
  • किसी मौजूदा किताब का नाम बदलकर उसे अपडेट करें:
$ curl -XPUT \
       -d '{"title":"Book"}' \
       -H "Content-Type: application/json" \      
       http://localhost:8080/books/9780003701203
  • किताबों की सूची फिर से पाएं (पहली 10):
$ curl http://localhost:8080/books
  • किसी खास लेखक की किताबें ढूंढें:
$ curl http://localhost:8080/books?author=Virginia+Woolf
  • अंग्रेज़ी में लिखी गई किताबों की सूची बनाएं:
$ curl http://localhost:8080/books?language=English
  • किताबों का चौथा पेज लोड करें:
$ curl http://localhost:8080/books?page=3

हम अपनी खोज को बेहतर बनाने के लिए author, language, और books क्वेरी पैरामीटर को भी जोड़ सकते हैं.

कंटेनर के हिसाब से REST API को बनाना और डिप्लॉय करना

हमें खुशी है कि REST API प्लान के मुताबिक काम करता है, इसलिए इसे Cloud Run पर क्लाउड में डिप्लॉय करने के लिए यही सही समय है!

हम इसे दो चरणों में करने वाले हैं:

  • सबसे पहले, नीचे दिए गए कमांड के साथ, Cloud Build के साथ कंटेनर इमेज बनाकर:
$ gcloud builds submit \
         --tag gcr.io/${GOOGLE_CLOUD_PROJECT}/crud-web-api
  • फिर, इस दूसरे कमांड से सेवा को डिप्लॉय करके:
$ gcloud run deploy run-crud \
         --image gcr.io/${GOOGLE_CLOUD_PROJECT}/crud-web-api \
         --allow-unauthenticated \
         --region=${REGION} \
         --platform=managed

पहले निर्देश की मदद से, Cloud Build कंटेनर इमेज बनाता है और उसे कंटेनर रजिस्ट्री में होस्ट करता है. अगला निर्देश, रजिस्ट्री से कंटेनर इमेज को डिप्लॉय करता है और उसे क्लाउड क्षेत्र में डिप्लॉय करता है.

हम Cloud Console के यूज़र इंटरफ़ेस (यूआई) में इस बात की दोबारा जांच कर सकते हैं कि हमारी क्लाउड रन सेवा अब सूची में दिख रही है या नहीं:

f62fbca02a8127c0.png

यहां दिए गए निर्देश की मदद से, हाल ही में डिप्लॉय की गई Cloud Run सेवा का यूआरएल वापस पाया जा सकता है:

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

हमें अगले सेक्शन में अपने Cloud Run REST API के यूआरएल की ज़रूरत होगी, क्योंकि हमारा App Engine फ़्रंटएंड कोड, एपीआई के साथ इंटरैक्ट करेगा.

9. लाइब्रेरी ब्राउज़ करने के लिए, किसी वेब ऐप्लिकेशन को होस्ट करना

इस प्रोजेक्ट में कुछ ग्लिटर जोड़ने वाली पहेली का आखिरी हिस्सा है, एक वेब फ़्रंटएंड उपलब्ध कराना जो हमारे REST API से इंटरैक्ट करेगा. इस काम के लिए, हम कुछ क्लाइंट JavaScript कोड के साथ Google App Engine का इस्तेमाल करेंगे. यह एपीआई को AJAX अनुरोधों के ज़रिए कॉल करेगा (क्लाइंट-साइड फे़च एपीआई का इस्तेमाल करके).

हालांकि, हमारा ऐप्लिकेशन Node.JS App Engine रनटाइम पर डिप्लॉय किया जाता है, लेकिन यह ज़्यादातर स्टैटिक संसाधनों से बना होता है! बैकएंड कोड उपलब्ध नहीं है. इसकी वजह यह है कि उपयोगकर्ता के ज़्यादातर इंटरैक्शन, ब्राउज़र में क्लाइंट-साइड JavaScript के ज़रिए होते हैं. हम किसी भी फ़ैंसी फ़्रंटएंड JavaScript फ़्रेमवर्क का इस्तेमाल नहीं करेंगे, हम सिर्फ़ "वैनिला" JavaScript का इस्तेमाल करेंगे. इसमें शूलेस वेब कॉम्पोनेंट लाइब्रेरी का इस्तेमाल करके, यूज़र इंटरफ़ेस (यूआई) के लिए कुछ वेब कॉम्पोनेंट इस्तेमाल किए गए हैं:

  • किताब की भाषा चुनने के लिए, चेकबॉक्स पर सही का निशान लगाएं:

6fb9f741000a2dc1.png

  • किसी खास किताब की जानकारी दिखाने के लिए एक कार्ड कॉम्पोनेंट (जिसमें JsBarcode लाइब्रेरी का इस्तेमाल करके, किताब का ISBN दिखाने वाला बारकोड शामिल है):

3aa21a9e16e3244e.png

  • और डेटाबेस से और किताबें लोड करने के लिए बटन:

3925ad81c91bbac9.png

उन सभी विज़ुअल कॉम्पोनेंट को एक साथ जोड़ते समय, हमारी लाइब्रेरी को ब्राउज़ करने के लिए, वेब पेज इस तरह दिखेगा:

18a5117150977d6.png

app.yaml कॉन्फ़िगरेशन फ़ाइल

इस App Engine ऐप्लिकेशन की app.yaml कॉन्फ़िगरेशन फ़ाइल को देखकर, इसके कोड बेस के बारे में जानना शुरू करें. यह फ़ाइल खास तौर पर App Engine के लिए बनाई गई है. इसकी मदद से एनवायरमेंट वैरिएबल, ऐप्लिकेशन के कई "हैंडलर" या यह बताया जा सकता है कि कुछ संसाधन स्टैटिक ऐसेट हैं, जिन्हें App Engine के बिल्ट-इन सीडीएन से इस्तेमाल किया जाएगा.

runtime: nodejs14

env_variables:
  RUN_CRUD_SERVICE_URL: CHANGE_ME

handlers:

- url: /js
  static_dir: public/js

- url: /css
  static_dir: public/css

- url: /img
  static_dir: public/img

- url: /(.+\.html)
  static_files: public/html/\1
  upload: public/(.+\.html)

- url: /
  static_files: public/html/index.html
  upload: public/html/index\.html

- url: /.*
  secure: always
  script: auto

हमने बताया है कि हमारा ऐप्लिकेशन Node.JS वाला है और हम वर्शन 14 का इस्तेमाल करना चाहते हैं.

इसके बाद, हम एक ऐसा एनवायरमेंट वैरिएबल तय करते हैं जो हमारी Cloud Run सेवा के यूआरएल पर ले जाता है. हमें CHANGE_ME प्लेसहोल्डर को सही URL से अपडेट करना होगा (इसे बदलने का तरीका नीचे देखें).

इसके बाद, हम अलग-अलग हैंडलर तय करते हैं. पहली तीन फ़ाइलें, public/ फ़ोल्डर और इसके सब-फ़ोल्डर में एचटीएमएल, सीएसएस, और JavaScript क्लाइंट-साइड कोड की जगह की जानकारी देती हैं. चौथा यह बताता है कि हमारे App Engine ऐप्लिकेशन का रूट यूआरएल, index.html पेज की ओर इशारा करता है. इस तरह, वेबसाइट के रूट को ऐक्सेस करने पर, हमें यूआरएल में index.html सफ़िक्स नहीं दिखेगा. और आखिरी विकल्प डिफ़ॉल्ट यूआरएल है, जो दूसरे सभी यूआरएल (/.*) को हमारे Node.JS ऐप्लिकेशन (जैसे कि ऐप्लिकेशन के "डाइनैमिक" हिस्से को, हमारे बताए गए स्टैटिक एसेट पर रूट करेगा).

आइए, अब Cloud Run सेवा के Web API यूआरएल को अपडेट करें.

appengine-frontend/ डायरेक्ट्री में, हमारे Cloud Run-आधारित REST API के यूआरएल पर पॉइंट करने वाले एनवायरमेंट वैरिएबल को अपडेट करने के लिए, नीचे दिया गया कमांड चलाएं:

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

इसके अलावा, app.yaml में CHANGE_ME स्ट्रिंग को सही यूआरएल के साथ मैन्युअल तरीके से बदलें:

env_variables:
    RUN_CRUD_SERVICE_URL: CHANGE_ME

Node.JS package.json फ़ाइल

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

हम फिर से बताते हैं कि Node.JS 14 का इस्तेमाल करके, इस ऐप्लिकेशन को चलाना है. किताबों की पुष्टि करने के लिए, हम एक्सप्रेस फ़्रेमवर्क और isbn3 एनपीएम मॉड्यूल पर निर्भर होते हैं ISBN कोड.

डेवलपमेंट डिपेंडेंसी में, हम फ़ाइल में होने वाले बदलावों पर नज़र रखने के लिए, nodemon मॉड्यूल का इस्तेमाल करेंगे. हालांकि, हम npm start का इस्तेमाल करके, ऐप्लिकेशन को स्थानीय तौर पर चला सकते हैं. हालांकि, कोड में कुछ बदलाव करके, ^C वाले ऐप्लिकेशन को बंद करने के बाद, फिर से लॉन्च करने के बाद, यह थोड़ा मुश्किल हो जाता है. इसके बजाय, ऐप्लिकेशन में बदलाव करने पर वह अपने-आप फिर से लोड / रीस्टार्ट हो सके, इसके लिए हम इन कमांड का इस्तेमाल कर सकते हैं:

$ npm run dev

index.js Node.JS कोड

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

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

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

हमें एक्सप्रेस वेब फ़्रेमवर्क की ज़रूरत है. हम तय करते हैं कि सार्वजनिक डायरेक्ट्री में स्टैटिक ऐसेट होती हैं, जिन्हें static मिडलवेयर से पेश किया जा सकता है (कम से कम तब, जब लोकल तौर पर डेवलपमेंट मोड में चलाया जा रहा हो). आखिर में, हमारे JSON पेलोड को पार्स करने के लिए, body-parser की ज़रूरत होती है.

आइए, हमारे कुछ तय किए गए रास्तों पर नज़र डालते हैं:

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

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

/ से मेल खाने वाला पहला विकल्प, हमारी public/html डायरेक्ट्री में मौजूद index.html पर रीडायरेक्ट करेगा. डेवलपमेंट मोड में, हम App Engine रनटाइम के दौरान काम नहीं करते. इसलिए, हम App Engine की यूआरएल रूटिंग को प्रोसेस नहीं कर पाते. इसके बजाय, यहां हम रूट यूआरएल को एचटीएमएल फ़ाइल पर रीडायरेक्ट कर रहे हैं.

हम जिस दूसरे एंडपॉइंट को /webapi तय करते हैं वह हमारे Cloud RUN REST API का यूआरएल दिखाएगा. इस तरह, क्लाइंट-साइड JavaScript कोड को यह पता चल जाएगा कि किताबों की सूची पाने के लिए कहां कॉल करना है.

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

प्रक्रिया को पूरा करने के लिए, हम डिफ़ॉल्ट रूप से Express वेब ऐप्लिकेशन चला रहे हैं और पोर्ट 8080 पर सुन रहे हैं.

index.html पेज

हम इस लंबे एचटीएमएल पेज की हर लाइन को नहीं देखेंगे. इसके बजाय, कुछ अहम लाइनों को हाइलाइट करते हैं.

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

पहली दो लाइनें शूलेस वेब कॉम्पोनेंट लाइब्रेरी (स्क्रिप्ट और स्टाइलशीट) को इंपोर्ट करती हैं.

अगली लाइन, JsBarcode लाइब्रेरी को इंपोर्ट करती है, ताकि किताब के ISBN कोड के बारकोड बनाए जा सकें.

आखिरी लाइनें, हमारे उस JavaScript कोड और सीएसएस स्टाइलशीट को इंपोर्ट कर रही हैं जो हमारी public/ सबडायरेक्ट्री में मौजूद हैं.

एचटीएमएल पेज के body में, हम शूलेस कॉम्पोनेंट का इस्तेमाल उनके कस्टम एलिमेंट टैग के साथ करते हैं, जैसे कि:

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

और हम किसी किताब को दिखाने के लिए एचटीएमएल टेंप्लेट और उनकी जगह भरने की क्षमता का भी इस्तेमाल करते हैं. हम किताबों की सूची को पॉप्युलेट करने के लिए उस टेंप्लेट की कॉपी बनाएंगे और स्लॉट में मौजूद वैल्यू को किताबों की जानकारी से बदल देंगे:

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

काफ़ी एचटीएमएल है, हमने कोड की समीक्षा करीब-करीब पूरी कर ली है. आखिरी हिस्सा बचा है: app.js क्लाइंट-साइड JavaScript कोड, जो हमारे REST API से इंटरैक्ट करता है.

app.js क्लाइंट-साइड JavaScript कोड

हम टॉप-लेवल इवेंट लिसनर के साथ शुरुआत करते हैं जो डीओएम कॉन्टेंट के लोड होने का इंतज़ार करता है:

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

इसके तैयार होने के बाद, हम कुछ मुख्य स्थिरांक और वैरिएबल सेट अप कर सकते हैं:

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

सबसे पहले, हम अपने REST API का यूआरएल फ़ेच करेंगे. इसके लिए, हम अपने App Engine नोड कोड को धन्यवाद देंगे. यह एनवायरमेंट वैरिएबल दिखाता है, जिसे हमने शुरुआत में app.yaml में सेट किया था. एनवायरमेंट वैरिएबल की वजह से, JavaScript क्लाइंट-साइड कोड से कॉल किया गया /webapi एंडपॉइंट. हमें अपने फ़्रंटएंड कोड में REST API यूआरएल को हार्डकोड नहीं करना पड़ा.

हम page और language वैरिएबल भी तय करते हैं. इनका इस्तेमाल, हम पेज पर नंबर डालने और भाषा के हिसाब से फ़िल्टर करने की गतिविधि को ट्रैक करने के लिए करेंगे.

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

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

हम किताबें लोड करने के लिए बटन पर एक इवेंट हैंडलर जोड़ते हैं. इस पर क्लिक करने पर, यह appendMoreBooks() फ़ंक्शन को कॉल करेगा.

    const langSelect = document.getElementById('language-select');
    langSelect.addEventListener('sl-change', event => {
        page = 0;
        language = event.srcElement.value;
        document.getElementById('library').replaceChildren();
        console.log(`Language selected: "${language}"`);

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

ऐसा ही एक बॉक्स होता है, जिसमें भाषा चुनने के विकल्प में होने वाले बदलावों की सूचना पाने के लिए, हम एक इवेंट हैंडलर जोड़ते हैं. बटन की तरह ही, हम REST API यूआरएल, मौजूदा पेज, और चुनी गई भाषा को पास करते हुए, appendMoreBooks() फ़ंक्शन को भी कॉल करते हैं.

आइए, जानते हैं कि किताबों को फ़ेच और जोड़ने वाले फ़ंक्शन का इस्तेमाल कैसे किया जाता है:

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

ऊपर, हम REST API को कॉल करने के लिए सटीक यूआरएल तैयार कर रहे हैं. आम तौर पर, हम तीन क्वेरी पैरामीटर को तय कर सकते हैं, लेकिन यहां इस यूज़र इंटरफ़ेस (यूआई) में सिर्फ़ दो क्वेरी पैरामीटर के बारे में बताया गया है:

  • page — किताबों के पेजों पर नंबर डालने के लिए, मौजूदा पेज को दिखाने वाला पूर्णांक,
  • language — लिखी गई भाषा के हिसाब से फ़िल्टर करने के लिए भाषा की स्ट्रिंग.

इसके बाद, हम फे़च एपीआई का इस्तेमाल करके, उस JSON कलेक्शन को वापस लाते हैं जिसमें हमारी किताब की जानकारी मौजूद है.

    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 हेडर, रिस्पॉन्स में मौजूद है या नहीं, इसके आधार पर हम [More books...] बटन को दिखाएंगे या छिपाएं. Link हेडर से हमें यह संकेत मिलता है कि क्या अब भी कुछ और किताबें लोड होनी बाकी हैं (Link हेडर में next यूआरएल होगा).

    const library = document.getElementById('library');
    const template = document.getElementById('book-card');
    for (let book of books) {
        const bookCard = template.content.cloneNode(true);

        bookCard.querySelector('slot[name=title]').innerText = book.title;
        bookCard.querySelector('slot[name=language]').innerText = book.language;
        bookCard.querySelector('slot[name=author]').innerText = book.author;
        bookCard.querySelector('slot[name=year]').innerText = book.year;
        bookCard.querySelector('slot[name=pages]').innerText = book.pages;
        
        const img = document.createElement('img');
        img.setAttribute('id', book.isbn);
        img.setAttribute('class', 'img-barcode-' + book.isbn)
        bookCard.querySelector('slot[name=barcode]').appendChild(img);

        library.appendChild(bookCard);
        ... 
    }
}

फ़ंक्शन के ऊपर दिए गए सेक्शन में, REST API से लौटाए गई हर किताब के लिए, हम किताब का प्रतिनिधित्व करने वाले कुछ वेब कॉम्पोनेंट के साथ टेंप्लेट का क्लोन बनाने वाले हैं और हम टेंप्लेट के स्लॉट में किताब की जानकारी अपने-आप भर रहे हैं.

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

ISBN कोड को थोड़ा बेहतर बनाने के लिए, हम JsBarcode लाइब्रेरी का इस्तेमाल करके एक अच्छा बारकोड बनाते हैं. जैसे, असली किताबों के पीछे के कवर पर!

ऐप्लिकेशन को स्थानीय तौर पर चलाना और उसकी जांच करना

अभी के लिए काफ़ी कोड, यह ऐप्लिकेशन को काम करते हुए देखने का समय है. सबसे पहले, हम इन्हें Cloud Shell में स्थानीय तौर पर डिप्लॉय करेंगे. इसके बाद, इन्हें असल में डिप्लॉय करेंगे.

हम अपने ऐप्लिकेशन के लिए ज़रूरी एनपीएम मॉड्यूल इंस्टॉल करते हैं:

$ npm install

इसके अलावा, हम ऐप्लिकेशन को सामान्य तरीके से चलाते हैं:

$ npm start

इसके अलावा, बदलावों को अपने-आप फिर से लोड करने के लिए, nodemon को धन्यवाद. इनके साथ:

$ npm run dev

ऐप्लिकेशन स्थानीय तौर पर चल रहा है और हम इसे http://localhost:8080 पर ब्राउज़र से ऐक्सेस कर सकते हैं.

App Engine ऐप्लिकेशन को डिप्लॉय करना

अब जब हमें यकीन है कि हमारा ऐप्लिकेशन स्थानीय तौर पर ठीक से काम कर रहा है, तो अब इसे App Engine पर डिप्लॉय करने का समय आ गया है.

ऐप्लिकेशन को डिप्लॉय करने के लिए, नीचे दिया गया कमांड लॉन्च करते हैं:

$ gcloud app deploy -q

करीब एक मिनट के बाद, ऐप्लिकेशन डिप्लॉय हो जाएगा.

ऐप्लिकेशन इस आकार के यूआरएल पर उपलब्ध होगा: https://${GOOGLE_CLOUD_PROJECT}.appspot.com.

हमारे App Engine वेब ऐप्लिकेशन के यूज़र इंटरफ़ेस (यूआई) को एक्सप्लोर करना

अब आप:

  • ज़्यादा किताबें लोड करने के लिए, [More books...] बटन पर क्लिक करें.
  • वह भाषा चुनें जिसमें आपको सिर्फ़ उसी भाषा की किताबें देखनी हैं.
  • सभी किताबों की सूची पर वापस आने के लिए, चुने गए बॉक्स में छोटे क्रॉस का इस्तेमाल करके, चुनी हुई किताबों को हटाएं.

10. स्टोरेज खाली करें (ज़रूरी नहीं)

अगर आपको ऐप्लिकेशन का इस्तेमाल नहीं करना है, तो संसाधनों को खाली किया जा सकता है. इससे प्रोजेक्ट को मिटाया जा सकता है और कम से कम खर्च किया जा सकता है. साथ ही, एक अच्छा क्लाउड सिटिज़न भी बन सकता है:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

11. बधाई हो!

हमने Cloud Functions, App Engine, और Cloud Run की मदद से कई तरह की सेवाओं का एक सेट तैयार किया है. इसकी मदद से अलग-अलग वेब एपीआई एंडपॉइंट और वेब फ़्रंटएंड दिखाया जा सकता है, ताकि किताबों की लाइब्रेरी को सेव, अपडेट, और ब्राउज़ किया जा सके. इसके लिए, REST API डेवलपमेंट के लिए कुछ अच्छे डिज़ाइन पैटर्न का पालन किया गया.

इसमें हमने इन विषयों के बारे में बताया

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

आगे बढ़ना

अगर आपको इस सटीक उदाहरण को समझना है और इसे ज़्यादा लोगों के लिए उपलब्ध कराना है, तो यहां दिए गए विषयों के बारे में जानें:

  • एपीआई गेटवे का इस्तेमाल करके, डेटा इंपोर्ट फ़ंक्शन और REST API कंटेनर में आम तौर पर इस्तेमाल किया जा सकने वाला एपीआई उपलब्ध कराएं. इससे, एपीआई ऐक्सेस करने के लिए, एपीआई पासकोड हैंडल करने जैसी सुविधाएं जोड़ी जा सकती हैं. इसके अलावा, एपीआई का इस्तेमाल करने वालों के लिए अनुरोध भेजने की दर तय की जा सकती है.
  • REST API के लिए, टेस्ट प्लेग्राउंड ऑफ़र करने और उसका दस्तावेज़ देने के लिए, App Engine ऐप्लिकेशन में स्वैगर-यूआई नोड मॉड्यूल डिप्लॉय करें.
  • फ़्रंटएंड पर, मौजूदा ब्राउज़िंग क्षमता के अलावा, डेटा में बदलाव करने के लिए अतिरिक्त स्क्रीन जोड़ें, नई किताबों की एंट्री बनाएं. साथ ही, हम Cloud Firestore डेटाबेस का इस्तेमाल कर रहे हैं, इसलिए इसकी रीयल-टाइम सुविधा का इस्तेमाल करके, किताब के डेटा को अपडेट करते समय अपडेट करें.