সার্ভারহীন ওয়েব এপিআই ওয়ার্কশপ

১. সংক্ষিপ্ত বিবরণ

এই কোডল্যাবের উদ্দেশ্য হলো গুগল ক্লাউড প্ল্যাটফর্মের 'সার্ভারলেস' পরিষেবাগুলো সম্পর্কে অভিজ্ঞতা অর্জন করা।

  • ক্লাউড ফাংশন — ফাংশন আকারে ব্যবসায়িক যুক্তির ক্ষুদ্র অংশ স্থাপন করার জন্য, যা বিভিন্ন ঘটনার (পাব/সাব বার্তা, ক্লাউড স্টোরেজে নতুন ফাইল, HTTP অনুরোধ, এবং আরও অনেক কিছু) প্রতিক্রিয়ায় কাজ করে।
  • অ্যাপ ইঞ্জিন — দ্রুত স্কেল আপ এবং ডাউন করার ক্ষমতাসহ ওয়েব অ্যাপ, ওয়েব এপিআই, মোবাইল ব্যাকএন্ড, স্ট্যাটিক অ্যাসেট ডেপ্লয় এবং সার্ভ করার জন্য।
  • ক্লাউড রান — কন্টেইনার স্থাপন ও স্কেল করার জন্য, যেগুলোতে যেকোনো ভাষা, রানটাইম বা লাইব্রেরি থাকতে পারে।

এবং সেই সার্ভারলেস সার্ভিসগুলো ব্যবহার করে কীভাবে ওয়েব ও REST API ডেপ্লয় এবং স্কেল করা যায়, তা আবিষ্কার করা; পাশাপাশি এই প্রক্রিয়ায় কিছু ভালো RESTful ডিজাইন নীতিও দেখা।

এই কর্মশালায় আমরা নিম্নলিখিত উপাদানগুলো নিয়ে একটি বুকশেলফ এক্সপ্লোরার তৈরি করব:

  • একটি ক্লাউড ফাংশন: আমাদের লাইব্রেরিতে উপলব্ধ বইগুলির প্রাথমিক ডেটাসেটটি ক্লাউড ফায়ারস্টোর ডকুমেন্ট ডেটাবেসে ইম্পোর্ট করার জন্য,
  • একটি ক্লাউড রান কন্টেইনার: যা আমাদের ডেটাবেসের বিষয়বস্তুর উপর একটি REST API উন্মুক্ত করবে,
  • একটি অ্যাপ ইঞ্জিন ওয়েব ফ্রন্টএন্ড: আমাদের REST API কল করে বইয়ের তালিকাটি ব্রাউজ করার জন্য।

এই কোডল্যাবটি শেষে ওয়েব ফ্রন্টএন্ডটি দেখতে এইরকম হবে:

705e014da0ca5e90.png

আপনি যা শিখবেন

  • ক্লাউড ফাংশন
  • ক্লাউড ফায়ারস্টোর
  • ক্লাউড রান
  • অ্যাপ ইঞ্জিন

২. সেটআপ এবং প্রয়োজনীয়তা

স্ব-গতিতে পরিবেশ সেটআপ

  1. Google Cloud Console- এ সাইন-ইন করুন এবং একটি নতুন প্রজেক্ট তৈরি করুন অথবা বিদ্যমান কোনো প্রজেক্ট পুনরায় ব্যবহার করুন। যদি আপনার আগে থেকে Gmail বা Google Workspace অ্যাকাউন্ট না থাকে, তবে আপনাকে অবশ্যই একটি তৈরি করতে হবে।

295004821bab6a87.png

37d264871000675d.png

96d86d3d5655cdbe.png

  • প্রজেক্টের নামটি হলো এই প্রজেক্টের অংশগ্রহণকারীদের প্রদর্শিত নাম। এটি একটি ক্যারেক্টার স্ট্রিং যা গুগল এপিআই ব্যবহার করে না। আপনি যেকোনো সময় এটি আপডেট করতে পারেন।
  • প্রজেক্ট আইডি সমস্ত গুগল ক্লাউড প্রজেক্ট জুড়ে অনন্য এবং অপরিবর্তনীয় (একবার সেট করার পর এটি পরিবর্তন করা যায় না)। ক্লাউড কনসোল স্বয়ংক্রিয়ভাবে একটি অনন্য স্ট্রিং তৈরি করে; সাধারণত এটি কী তা নিয়ে আপনার মাথা ঘামানোর দরকার নেই। বেশিরভাগ কোডল্যাবে, আপনাকে আপনার প্রজেক্ট আইডি উল্লেখ করতে হবে (যা সাধারণত PROJECT_ID হিসাবে চিহ্নিত করা হয়)। তৈরি করা আইডিটি আপনার পছন্দ না হলে, আপনি এলোমেলোভাবে আরেকটি তৈরি করতে পারেন। বিকল্পভাবে, আপনি আপনার নিজের আইডি দিয়ে চেষ্টা করে দেখতে পারেন যে সেটি উপলব্ধ আছে কিনা। এই ধাপের পরে এটি পরিবর্তন করা যাবে না এবং প্রজেক্টের পুরো সময়কাল জুড়ে এটি অপরিবর্তিত থাকবে।
  • আপনার অবগতির জন্য জানাচ্ছি যে, তৃতীয় একটি ভ্যালু রয়েছে, যা হলো প্রজেক্ট নম্বর , এবং কিছু এপিআই এটি ব্যবহার করে থাকে। ডকুমেন্টেশনে এই তিনটি ভ্যালু সম্পর্কে আরও বিস্তারিত জানুন।
  1. এরপর, ক্লাউড রিসোর্স/এপিআই ব্যবহার করার জন্য আপনাকে ক্লাউড কনসোলে বিলিং চালু করতে হবে। এই কোডল্যাবটি সম্পন্ন করতে খুব বেশি খরচ হবে না, এমনকি আদৌ কোনো খরচ নাও হতে পারে। এই টিউটোরিয়ালের পর বিলিং এড়াতে রিসোর্সগুলো বন্ধ করার জন্য, আপনি আপনার তৈরি করা রিসোর্সগুলো অথবা প্রজেক্টটি ডিলিট করে দিতে পারেন। নতুন গুগল ক্লাউড ব্যবহারকারীরা ৩০০ মার্কিন ডলারের ফ্রি ট্রায়াল প্রোগ্রামের জন্য যোগ্য।

ক্লাউড শেল শুরু করুন

যদিও গুগল ক্লাউড আপনার ল্যাপটপ থেকে দূরবর্তীভাবে পরিচালনা করা যায়, এই কোডল্যাবে আপনি গুগল ক্লাউড শেল ব্যবহার করবেন, যা ক্লাউডে চালিত একটি কমান্ড লাইন পরিবেশ।

গুগল ক্লাউড কনসোল থেকে, উপরের ডানদিকের টুলবারে থাকা ক্লাউড শেল আইকনটিতে ক্লিক করুন:

84688aa223b1c3a2.png

পরিবেশটি প্রস্তুত করতে এবং এর সাথে সংযোগ স্থাপন করতে মাত্র কয়েক মুহূর্ত সময় লাগবে। এটি শেষ হলে, আপনি এইরকম কিছু দেখতে পাবেন:

320e18fedb7fbe0.png

এই ভার্চুয়াল মেশিনটিতে আপনার প্রয়োজনীয় সমস্ত ডেভেলপমেন্ট টুলস লোড করা আছে। এটি একটি স্থায়ী ৫ জিবি হোম ডিরেক্টরি প্রদান করে এবং গুগল ক্লাউডে চলে, যা নেটওয়ার্ক পারফরম্যান্স ও অথেনটিকেশনকে ব্যাপকভাবে উন্নত করে। এই কোডল্যাবে আপনার সমস্ত কাজ একটি ব্রাউজারের মধ্যেই করা যাবে। আপনাকে কিছুই ইনস্টল করতে হবে না।

৩. পরিবেশ প্রস্তুত করুন এবং ক্লাউড এপিআই সক্রিয় করুন

এই প্রোজেক্ট জুড়ে আমাদের প্রয়োজনীয় বিভিন্ন পরিষেবা ব্যবহার করার জন্য, আমরা কয়েকটি এপিআই (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

যেহেতু আমরা ক্লাউড ফায়ারস্টোর ডেটাবেসে ডেটা সংরক্ষণ করব, তাই আমাদের ডেটাবেসটি তৈরি করতে হবে:

$ 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 

এই তিনটি ইনডেক্স লেখক বা ভাষা অনুযায়ী আমাদের অনুসন্ধানের সাথে সম্পর্কিত, এবং একটি আপডেট করা ফিল্ডের মাধ্যমে সংগ্রহে ক্রম বজায় রাখে।

৪. কোডটি সংগ্রহ করুন

নিম্নলিখিত গিটহাব রিপোজিটরি থেকে কোডটি সংগ্রহ করুন:

$ 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 — এই ফোল্ডারটিতে ১০০টি বইয়ের একটি তালিকার নমুনা ডেটা রয়েছে।
  • function-import — এই ফাংশনটি নমুনা ডেটা ইম্পোর্ট করার জন্য একটি এন্ডপয়েন্ট প্রদান করবে।
  • run-crud — এই কন্টেইনারটি ক্লাউড ফায়ারস্টোরে সংরক্ষিত বইয়ের ডেটা অ্যাক্সেস করার জন্য একটি ওয়েব এপিআই উন্মুক্ত করবে।
  • appengine-frontend — এই অ্যাপ ইঞ্জিন ওয়েব অ্যাপ্লিকেশনটি বইয়ের তালিকা ব্রাউজ করার জন্য একটি সাধারণ রিড-অনলি ফ্রন্টএন্ড প্রদর্শন করবে।

৫. নমুনা বইয়ের গ্রন্থাগারের তথ্য

ডেটা ফোল্ডারে আমাদের একটি books.json ফাইল আছে, যাতে সম্ভবত পড়ার মতো একশটি বইয়ের একটি তালিকা রয়েছে। এই JSON ডকুমেন্টটি হলো JSON অবজেক্ট ধারণকারী একটি অ্যারে। চলুন, ক্লাউড ফাংশনের মাধ্যমে আমরা যে ডেটা ইনজেস্ট করব তার গঠনটি দেখে নেওয়া যাক:

[
  {
    "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 — বইটি শনাক্তকারী আইএসবিএন-১৩ কোড।
  • author — বইটির লেখকের নাম।
  • language — যে কথ্য ভাষায় বইটি লেখা হয়েছে।
  • pages — বইটিতে থাকা পৃষ্ঠার সংখ্যা।
  • title — বইটির শিরোনাম।
  • year — যে বছর বইটি প্রকাশিত হয়েছিল।

৬. নমুনা বইয়ের ডেটা ইম্পোর্ট করার জন্য একটি ফাংশন এন্ডপয়েন্ট

এই প্রথম অংশে, আমরা নমুনা বইয়ের ডেটা ইম্পোর্ট করার জন্য ব্যবহৃত এন্ডপয়েন্টটি তৈরি করব। এই উদ্দেশ্যে আমরা ক্লাউড ফাংশন ব্যবহার করব।

কোডটি অন্বেষণ করুন

চলুন 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 NPM মডিউলটি প্রয়োজন। অভ্যন্তরীণভাবে, ক্লাউড ফাংশনস রানটাইম এক্সপ্রেস ওয়েব ফ্রেমওয়ার্কও সরবরাহ করে, তাই এটিকে ডিপেন্ডেন্সি হিসেবে ঘোষণা করার প্রয়োজন নেই।

ডেভেলপমেন্ট ডিপেন্ডেন্সিতে, আমরা ফাংশনস ফ্রেমওয়ার্ক ( @google-cloud/functions-framework ) ঘোষণা করি, যা আপনার ফাংশনগুলোকে কল করার জন্য ব্যবহৃত রানটাইম ফ্রেমওয়ার্ক। এটি একটি ওপেন সোর্স ফ্রেমওয়ার্ক যা আপনি আপনার মেশিনে স্থানীয়ভাবেও (আমাদের ক্ষেত্রে, ক্লাউড শেলের ভিতরে) ব্যবহার করতে পারেন। এর ফলে প্রতিবার পরিবর্তনের পর ডিপ্লয় না করেই ফাংশনগুলো চালানো যায়, যা ডেভেলপমেন্ট ফিডব্যাক লুপকে উন্নত করে।

নির্ভরতাগুলি ইনস্টল করতে, install কমান্ডটি ব্যবহার করুন:

$ npm install

start স্ক্রিপ্টটি ফাংশনস ফ্রেমওয়ার্ক ব্যবহার করে আপনাকে এমন একটি কমান্ড দেয়, যা দিয়ে আপনি নিম্নলিখিত নির্দেশনা অনুসরণ করে ফাংশনটি স্থানীয়ভাবে চালাতে পারবেন:

$ npm start

ফাংশনটির সাথে ইন্টারঅ্যাক্ট করার জন্য আপনি `curl` অথবা HTTP GET রিকোয়েস্টের জন্য `Cloud Shell` ওয়েব প্রিভিউ ব্যবহার করতে পারেন।

চলুন এবার index.js ফাইলটি দেখে নেওয়া যাক, যেখানে আমাদের বইয়ের ডেটা ইম্পোর্ট ফাংশনের লজিক রয়েছে:

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

আমরা ফায়ারস্টোর মডিউলটি ইনস্ট্যানশিয়েট করি এবং 'books' কালেকশনটিকে নির্দেশ করি (রিলেশনাল ডেটাবেসের টেবিলের মতো)।

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 জাভাস্ক্রিপ্ট ফাংশনটি এক্সপোর্ট করছি। পরবর্তীতে যখন আমরা এটি ডিপ্লয় করব, তখন এই ফাংশনটিই ডিক্লেয়ার করব।

পরবর্তী দুটি নির্দেশনা যাচাই করছে যে:

  • আমরা শুধুমাত্র HTTP POST অনুরোধ গ্রহণ করছি এবং অন্যথায় অন্যান্য HTTP পদ্ধতি অনুমোদিত নয় তা বোঝাতে একটি 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 পেলোডটি পেতে পারি। আমরা সমস্ত বই একসাথে সংরক্ষণ করার জন্য একটি ফায়ারস্টোর ব্যাচ অপারেশন প্রস্তুত করছি। আমরা বইয়ের বিবরণ সম্বলিত 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 স্ট্যাটাস কোড রিটার্ন করি। অন্যথায়, আমরা একটি OK রেসপন্স রিটার্ন করতে পারি, সাথে একটি 202 স্ট্যাটাস কোড যা নির্দেশ করে যে বাল্ক সেভ রিকোয়েস্টটি গৃহীত হয়েছে।

ইম্পোর্ট ফাংশনটি চালানো এবং পরীক্ষা করা

কোডটি চালানোর আগে, আমরা নিম্নলিখিত কমান্ডের মাধ্যমে ডিপেন্ডেন্সিগুলো ইনস্টল করব:

$ npm install

ফাংশনটি স্থানীয়ভাবে চালানোর জন্য, ফাংশনস ফ্রেমওয়ার্কের সৌজন্যে, আমরা package.json এ সংজ্ঞায়িত start স্ক্রিপ্ট কমান্ডটি ব্যবহার করব:

$ npm start

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

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

আপনার লোকাল ফাংশনে একটি HTTP POST রিকোয়েস্ট পাঠাতে, আপনি এটি চালাতে পারেন:

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

এই কমান্ডটি চালু করলে, আপনি নিম্নলিখিত আউটপুট দেখতে পাবেন, যা নিশ্চিত করে যে ফাংশনটি স্থানীয়ভাবে চলছে:

{"status":"OK"}

ডেটা সত্যিই ফায়ারস্টোরে সংরক্ষিত হয়েছে কিনা, তা যাচাই করতে আপনি ক্লাউড কনসোল UI-তেও যেতে পারেন:

409982568cebdbf8.png

উপরের স্ক্রিনশটে আমরা তৈরি করা books সংগ্রহ, বইয়ের ISBN কোড দ্বারা চিহ্নিত বইয়ের নথিপত্রের তালিকা এবং ডানদিকে সেই নির্দিষ্ট বইয়ের এন্ট্রির বিবরণ দেখতে পাচ্ছি।

ক্লাউডে ফাংশনটি স্থাপন করা

ক্লাউড ফাংশনসে ফাংশনটি ডিপ্লয় করতে, আমরা 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 নামক একটি সাংকেতিক নাম দিয়ে ফাংশনটি স্থাপন করি। এই ফাংশনটি HTTP অনুরোধের মাধ্যমে সক্রিয় হয়। আমরা Node.JS 20 রানটাইম ব্যবহার করি। আমরা ফাংশনটি সর্বজনীনভাবে স্থাপন করি (আদর্শগতভাবে, আমাদের সেই এন্ডপয়েন্টটি সুরক্ষিত করা উচিত)। আমরা সেই অঞ্চলটি নির্দিষ্ট করে দিই যেখানে আমরা ফাংশনটি রাখতে চাই। এবং আমরা স্থানীয় ডিরেক্টরিতে থাকা সোর্সগুলোকে নির্দেশ করি ও এন্ট্রি পয়েন্ট হিসেবে parseBooks (এক্সপোর্ট করা জাভাস্ক্রিপ্ট ফাংশন) ব্যবহার করি।

দুই মিনিট বা তারও কম সময়ের মধ্যে ফাংশনটি ক্লাউডে ডেপ্লয় করা হয়। ক্লাউড কনসোল UI-তে আপনি ফাংশনটি দেখতে পাবেন:

c910875d4dc0aaa8.png

ডিপ্লয়মেন্ট আউটপুটে, আপনি আপনার ফাংশনের URL দেখতে পাবেন, যা একটি নির্দিষ্ট নামকরণ রীতি ( https://${REGION}-${GOOGLE_CLOUD_PROJECT}.cloudfunctions.net/${FUNCTION_NAME} ) অনুসরণ করে, এবং অবশ্যই, আপনি এই HTTP ট্রিগার URL-টি ক্লাউড কনসোল UI-এর ট্রিগার ট্যাবেও খুঁজে পাবেন:

380ffc46eb56441e.png

আপনি gcloud ব্যবহার করে কমান্ড-লাইন থেকেও URL-টি পেতে পারেন:

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

চলুন এটিকে BULK_IMPORT_URL এনভায়রনমেন্ট ভেরিয়েবলে সংরক্ষণ করি, যাতে আমরা আমাদের ডেপ্লয় করা ফাংশনটি পরীক্ষা করার জন্য এটি পুনরায় ব্যবহার করতে পারি।

স্থাপন করা ফাংশন পরীক্ষা করা হচ্ছে

ফাংশনটি স্থানীয়ভাবে চালানোর পরীক্ষা করার জন্য আমরা আগে যে কার্ল (curl) কমান্ডটি ব্যবহার করেছিলাম, তার অনুরূপ একটি কমান্ড দিয়েই আমরা ডেপ্লয় করা ফাংশনটি পরীক্ষা করব। একমাত্র পরিবর্তন হবে ইউআরএল (URL):

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

আবার, সফল হলে, এটি নিম্নলিখিত আউটপুট প্রদান করবে:

{"status":"OK"}

এখন যেহেতু আমাদের ইম্পোর্ট ফাংশনটি ডেপ্লয় করা ও প্রস্তুত এবং আমরা আমাদের স্যাম্পল ডেটা আপলোড করে ফেলেছি, তাই এই ডেটাসেটটিকে প্রকাশ করার জন্য REST API তৈরি করার সময় এসেছে।

৭. REST API চুক্তি

যদিও আমরা, উদাহরণস্বরূপ, ওপেন এপিআই স্পেসিফিকেশন ব্যবহার করে কোনো এপিআই কন্ট্রাক্ট নির্ধারণ করছি না, তবুও আমরা আমাদের REST API-এর বিভিন্ন এন্ডপয়েন্টগুলো দেখে নেব।

এপিআইটি বইয়ের JSON অবজেক্ট আদান-প্রদান করে, যার মধ্যে রয়েছে:

  • isbn (ঐচ্ছিক) — একটি বৈধ আইএসবিএন কোড নির্দেশকারী ১৩ অক্ষরের একটি String ,
  • 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
  }

GET /books

সমস্ত বইয়ের তালিকা পান, যা লেখক এবং/অথবা ভাষা অনুযায়ী ফিল্টার করা যেতে পারে এবং প্রতিবারে ১০টি ফলাফলের উইন্ডোতে পৃষ্ঠাবদ্ধ করা থাকে।

দেহে কোনো পেলোড নেই।

কোয়েরি প্যারামিটারসমূহ:

  • author (ঐচ্ছিক) — লেখকের নাম অনুসারে বইয়ের তালিকা ফিল্টার করে,
  • language (ঐচ্ছিক) — ভাষা অনুযায়ী বইয়ের তালিকা ফিল্টার করে।
  • page (ঐচ্ছিক, ডিফল্ট = ০) — ফেরত দেওয়া ফলাফলের পৃষ্ঠার ক্রম নির্দেশ করে।

ফেরত দেয়: বইয়ের অবজেক্টগুলোর একটি JSON অ্যারে।

স্ট্যাটাস কোড:

  • 200 — যখন বইয়ের তালিকা আনার অনুরোধটি সফল হয়,
  • 400 — যদি কোনো ত্রুটি ঘটে।

POST /books এবং POST /books/{isbn}

একটি নতুন বইয়ের পেলোড পোস্ট করুন, হয় isbn পাথ প্যারামিটার সহ (সেক্ষেত্রে বইয়ের পেলোডে isbn কোডের প্রয়োজন নেই) অথবা এটি ছাড়া (সেক্ষেত্রে বইয়ের পেলোডে isbn কোড অবশ্যই থাকতে হবে)।

দেহের সাথে থাকা বস্তু: একটি বই।

কোয়েরি প্যারামিটার: নেই।

ফেরত দেয়: কিছুই না।

স্ট্যাটাস কোড:

  • 201 — যখন বইটি সফলভাবে সংরক্ষণ করা হয়,
  • 406 — যদি isbn কোডটি অবৈধ হয়,
  • 400 — যদি কোনো ত্রুটি ঘটে।

GET /books/{isbn}

পাথ প্যারামিটার হিসেবে প্রদত্ত isbn কোডের মাধ্যমে লাইব্রেরি থেকে একটি বই খুঁজে বের করে।

দেহে কোনো পেলোড নেই।

কোয়েরি প্যারামিটার: নেই।

ফেরত দেয়: একটি বইয়ের JSON অবজেক্ট, অথবা বইটি বিদ্যমান না থাকলে একটি এরর অবজেক্ট।

স্ট্যাটাস কোড:

  • 200 — যদি বইটি ডাটাবেসে পাওয়া যায়,
  • 400 — যদি কোনো ত্রুটি ঘটে,
  • 404 — যদি বইটি খুঁজে না পাওয়া যায়,
  • 406 — যদি isbn কোডটি অবৈধ হয়।

PUT /books/{isbn}

পাথ প্যারামিটার হিসেবে প্রদত্ত isbn দ্বারা চিহ্নিত একটি বিদ্যমান বই আপডেট করে।

বডি পেলোড: একটি বই অবজেক্ট। শুধুমাত্র যে ফিল্ডগুলো আপডেট করা প্রয়োজন, সেগুলোই পাস করা যাবে; বাকিগুলো ঐচ্ছিক।

কোয়েরি প্যারামিটার: নেই।

রিটার্নস: হালনাগাদকৃত বই।

স্ট্যাটাস কোড:

  • 200 — যখন বইটি সফলভাবে হালনাগাদ করা হয়,
  • 400 — যদি কোনো ত্রুটি ঘটে,
  • 406 — যদি isbn কোডটি অবৈধ হয়।

/books/{isbn} মুছে ফেলুন

পাথ প্যারামিটার হিসেবে প্রদত্ত isbn দ্বারা চিহ্নিত একটি বিদ্যমান বই মুছে ফেলে।

দেহে কোনো পেলোড নেই।

কোয়েরি প্যারামিটার: নেই।

ফেরত দেয়: কিছুই না।

স্ট্যাটাস কোড:

  • 204 — যখন বইটি সফলভাবে মুছে ফেলা হয়,
  • 400 — যদি কোনো ত্রুটি ঘটে।

৮. একটি কন্টেইনারে 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 কমান্ডের সাহায্যে আমরা নির্দেশ করি যে এই অ্যাপ্লিকেশনটি কীভাবে রান করা হবে।

প্যাকেজ.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 এর ক্ষেত্রেও করা হয়েছিল।

আমাদের ওয়েব এপিআই অ্যাপ্লিকেশনটি নিম্নলিখিত বিষয়গুলির উপর নির্ভর করে:

  • ডাটাবেসে থাকা বইয়ের ডেটা অ্যাক্সেস করার জন্য ফায়ারস্টোর এনপিএম মডিউল,
  • CORS (Cross Origin Resource Sharing) অনুরোধগুলি পরিচালনা করার জন্য 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');

আমাদের ফায়ারস্টোর মডিউলটি প্রয়োজন এবং আমরা 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 ব্যবহার করছি। আমাদের API-এর সাথে আদান-প্রদান করা JSON পেলোডগুলো পার্স করার জন্য আমরা body-parser মডিউলটি ব্যবহার করছি।

URL পরিবর্তন করার জন্য querystring মডিউলটি সহায়ক। পেজিনেশনের উদ্দেশ্যে Link হেডার তৈরি করার সময় এটি কাজে লাগবে (এ বিষয়ে পরে আরও আলোচনা করা হবে)।

এরপর আমরা cors মডিউলটি কনফিগার করি। আমরা 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 NPM মডিউলটি ব্যবহার করব এবং আমরা একটি ছোট ইউটিলিটি ফাংশন তৈরি করব যা ISBN কোড পার্স করবে এবং কোডগুলো অবৈধ হলে রেসপন্সে একটি 406 স্ট্যাটাস কোড দেবে।

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

আমরা একটি কোয়েরি প্রস্তুত করার মাধ্যমে ডাটাবেসে কোয়েরি করার জন্য তৈরি হচ্ছি। এই কোয়েরিটি লেখক এবং/অথবা ভাষা অনুসারে ফিল্টার করার জন্য ঐচ্ছিক কোয়েরি প্যারামিটারগুলোর উপর নির্ভর করবে। এছাড়াও আমরা ১০টি করে বইয়ের খণ্ডে বইয়ের তালিকাটি ফেরত দিচ্ছি।

বইগুলো আনার সময় কোনো ত্রুটি হলে, আমরা 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 অনুসারে ফিল্টার করেছিলাম, কিন্তু এই অংশে আমরা বইয়ের তালিকাটি সর্বশেষ আপডেটের তারিখ অনুসারে সাজাবো (সর্বশেষ আপডেট করা বইটি প্রথমে আসবে)। এছাড়াও, আমরা একটি লিমিট (ফেরত দেওয়া উপাদানের সংখ্যা) এবং একটি অফসেট (যেখান থেকে পরবর্তী বইয়ের ব্যাচটি ফেরত দেওয়া শুরু হবে) নির্ধারণ করে ফলাফলটিকে পেজিনেট করব।

আমরা কোয়েরিটি এক্সিকিউট করে ডেটার একটি স্ন্যাপশট নিই এবং সেই ফলাফলগুলো একটি জাভাস্ক্রিপ্ট অ্যারেতে রাখি, যা ফাংশনের শেষে রিটার্ন করা হবে।

চলুন, একটি উত্তম অনুশীলন দেখার মাধ্যমে এই এন্ডপয়েন্টের ব্যাখ্যা শেষ করি: ডেটার প্রথম, পূর্ববর্তী, পরবর্তী বা শেষ পৃষ্ঠার URI লিঙ্ক নির্ধারণ করতে 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 কনস্ট্যান্ট দ্বারা নির্ধারিত সর্বাধিক সংখ্যক বই ধারণ করলে, এবং ধরে নেওয়া হয় যে আরও ডেটা সহ আরেকটি পৃষ্ঠা আসছে) আমরা একটি ' পরবর্তী ' লিঙ্ক যোগ করি। এরপর আমরা সঠিক সিনট্যাক্স ব্যবহার করে যথাযথ হেডার তৈরি করার জন্য এক্সপ্রেসের resource#links() ফাংশনটি ব্যবহার করি।

আপনার অবগতির জন্য, লিঙ্ক হেডারটি দেখতে অনেকটা এইরকম হবে:

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

উভয় এন্ডপয়েন্টই একটি নতুন বই তৈরি করার জন্য ব্যবহৃত হয়। একটিতে ISBN কোডটি বইয়ের পেলোডে (payload) পাঠানো হয়, অপরটিতে এটিকে একটি পাথ প্যারামিটার (path parameter) হিসেবে পাঠানো হয়। উভয় ক্ষেত্রেই, আমাদের 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 স্ট্যাটাস কোড সেট করি)। আমরা রিকোয়েস্টের বডিতে পাঠানো পেলোড থেকে বইয়ের ফিল্ডগুলো সংগ্রহ করি। এরপর আমরা বইয়ের বিবরণগুলো ফায়ারস্টোরে সংরক্ষণ করব। সফল হলে 201 এবং ব্যর্থ হলে 400 রিটার্ন করা হবে।

সফলভাবে রিটার্ন করার পর, আমরা লোকেশন হেডারটিও সেট করি, যাতে এপিআই-এর ক্লায়েন্টকে নতুন তৈরি হওয়া রিসোর্সটির অবস্থান সম্পর্কে ইঙ্গিত দেওয়া যায়। হেডারটি দেখতে নিম্নরূপ হবে:

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

চলুন, ফায়ারস্টোর থেকে 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-টি বৈধ কিনা তা যাচাই করি। বইটি খুঁজে বের করার জন্য আমরা ফায়ারস্টোরে একটি কোয়েরি পাঠাই। বইটি সত্যিই পাওয়া গেছে কিনা, তা জানার জন্য 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 হেডারটিও বইটির URI-কে নির্দেশ করার জন্য সেট করেছি।

  • 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 চালু হবে এবং ৮০৮০ পোর্টে লিসেন করবে।

নিম্নলিখিত কমান্ডগুলো ব্যবহার করে একটি ডকার কন্টেইনার তৈরি করা এবং কন্টেইনার ইমেজটি চালানোও সম্ভব:

$ docker build -t crud-web-api .

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

ডকারের মধ্যে অ্যাপ্লিকেশনটি চালানো একটি চমৎকার উপায়, যার মাধ্যমে এটিও পুনরায় যাচাই করা যায় যে ক্লাউড বিল্ড ব্যবহার করে ক্লাউডে অ্যাপ্লিকেশনটি তৈরি করার সময় এর কন্টেইনারাইজেশন ঠিকঠাকভাবে চলবে।

এপিআই পরীক্ষা করা হচ্ছে

আমরা REST API কোডটি যেভাবে রান করি না কেন (সরাসরি Node-এর মাধ্যমে অথবা একটি Docker কন্টেইনার ইমেজের মাধ্যমে), এখন আমরা এর বিপরীতে কয়েকটি কোয়েরি চালাতে সক্ষম।

  • একটি নতুন বই তৈরি করুন (মূল পেলোডে আইএসবিএন):
$ 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
  • বইগুলোর তালিকাটি পুনরুদ্ধার করুন (প্রথম ১০টি):
$ 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 পরিকল্পনা অনুযায়ী কাজ করছে, তাই এটিকে ক্লাউডে, ক্লাউড রান-এ ডেপ্লয় করার এটাই সঠিক মুহূর্ত!

আমরা এটা দুটি ধাপে করব:

  • প্রথমে, নিম্নলিখিত কমান্ডের সাহায্যে ক্লাউড বিল্ড ব্যবহার করে কন্টেইনার ইমেজটি তৈরি করুন:
$ 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

প্রথম কমান্ডের মাধ্যমে ক্লাউড বিল্ড কন্টেইনার ইমেজটি তৈরি করে এবং কন্টেইনার রেজিস্ট্রি-তে হোস্ট করে। পরবর্তী কমান্ডটি রেজিস্ট্রি থেকে কন্টেইনার ইমেজটি ক্লাউড রিজিয়নে ডেপ্লয় করে।

আমরা ক্লাউড কনসোল UI-তে পুনরায় যাচাই করে দেখতে পারি যে আমাদের ক্লাউড রান পরিষেবাটি এখন তালিকায় দেখা যাচ্ছে কিনা:

f62fbca02a8127c0.png

এখানে আমাদের শেষ ধাপটি হলো, নিম্নলিখিত কমান্ডটির সাহায্যে সদ্য ডেপ্লয় করা ক্লাউড রান সার্ভিসের URL-টি সংগ্রহ করা:

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

পরবর্তী অংশে আমাদের ক্লাউড রান REST API-এর URL-টি প্রয়োজন হবে, কারণ আমাদের অ্যাপ ইঞ্জিন ফ্রন্টএন্ড কোড এই API-টির সাথে ইন্টারঅ্যাক্ট করবে।

৯. লাইব্রেরি ব্রাউজ করার জন্য একটি ওয়েব অ্যাপ হোস্ট করুন।

এই প্রজেক্টে আরও জৌলুস যোগ করার জন্য শেষ ধাপটি হলো একটি ওয়েব ফ্রন্টএন্ড তৈরি করা, যা আমাদের REST API-এর সাথে যোগাযোগ করবে। সেই উদ্দেশ্যে, আমরা Google App Engine ব্যবহার করব এবং এর সাথে কিছু ক্লায়েন্ট জাভাস্ক্রিপ্ট কোড থাকবে যা AJAX রিকোয়েস্টের মাধ্যমে (ক্লায়েন্ট-সাইড Fetch API ব্যবহার করে) API-কে কল করবে।

আমাদের অ্যাপ্লিকেশনটি Node.JS App Engine রানটাইমে ডেপ্লয় করা হলেও, এটি মূলত স্ট্যাটিক রিসোর্স দিয়েই তৈরি! এতে খুব বেশি ব্যাকএন্ড কোড নেই, কারণ ব্যবহারকারীর বেশিরভাগ ইন্টারঅ্যাকশন ক্লায়েন্ট-সাইড জাভাস্ক্রিপ্টের মাধ্যমে ব্রাউজারেই হবে। আমরা কোনো জাঁকজমকপূর্ণ ফ্রন্টএন্ড জাভাস্ক্রিপ্ট ফ্রেমওয়ার্ক ব্যবহার করব না, শুধু সাধারণ জাভাস্ক্রিপ্ট ব্যবহার করব এবং Shoelace ওয়েব কম্পোনেন্ট লাইব্রেরি ব্যবহার করে UI-এর জন্য কয়েকটি ওয়েব কম্পোনেন্ট যোগ করব।

  • বইটির ভাষা নির্বাচন করার জন্য একটি সিলেক্ট বক্স:

6fb9f741000a2dc1.png

  • একটি নির্দিষ্ট বই সম্পর্কে বিস্তারিত তথ্য প্রদর্শনের জন্য একটি কার্ড কম্পোনেন্ট ( JsBarcode লাইব্রেরি ব্যবহার করে বইটির ISBN বোঝানোর জন্য একটি বারকোড সহ):

3aa21a9e16e3244e.png

  • এবং ডাটাবেস থেকে আরও বই লোড করার জন্য একটি বাটন:

3925ad81c91bbac9.png

এই সমস্ত ভিজ্যুয়াল উপাদানগুলোকে একত্রিত করলে, আমাদের লাইব্রেরি ব্রাউজ করার জন্য তৈরি হওয়া ওয়েব পেজটি দেখতে নিম্নরূপ হবে:

18a5117150977d6.png

app.yaml কনফিগারেশন ফাইল

চলুন, এই অ্যাপ ইঞ্জিন অ্যাপ্লিকেশনটির কোডবেসে প্রবেশ করা শুরু করি এর app.yaml কনফিগারেশন ফাইলটি দেখার মাধ্যমে। এটি অ্যাপ ইঞ্জিনের জন্য একটি নির্দিষ্ট ফাইল, এবং এর মাধ্যমে এনভায়রনমেন্ট ভেরিয়েবল, অ্যাপ্লিকেশনটির বিভিন্ন "হ্যান্ডলার" কনফিগার করা যায়, অথবা কিছু রিসোর্সকে স্ট্যাটিক অ্যাসেট হিসেবে নির্দিষ্ট করা যায়, যেগুলো অ্যাপ ইঞ্জিনের বিল্ট-ইন সিডিএন (CDN) দ্বারা পরিবেশিত হবে।

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-ভিত্তিক এবং আমরা সংস্করণ ১৪ ব্যবহার করতে চাই।

এরপর আমরা একটি এনভায়রনমেন্ট ভেরিয়েবল নির্ধারণ করব যা আমাদের ক্লাউড রান সার্ভিস ইউআরএল-কে নির্দেশ করবে। আমাদের CHANGE_ME প্লেসহোল্ডারটি সঠিক ইউআরএল দিয়ে আপডেট করতে হবে (কীভাবে এটি পরিবর্তন করতে হয় তা নিচে দেখুন)।

এরপর, আমরা বিভিন্ন হ্যান্ডলার সংজ্ঞায়িত করি। প্রথম তিনটি হ্যান্ডলার public/ ফোল্ডার এবং এর সাব-ফোল্ডারগুলোর অধীনে থাকা HTML, CSS, এবং JavaScript ক্লায়েন্ট-সাইড কোডের অবস্থান নির্দেশ করে। চতুর্থটি নির্দেশ করে যে আমাদের App Engine অ্যাপ্লিকেশনের রুট URL-টি index.html পেজকে নির্দেশ করবে। এর ফলে, ওয়েবসাইটের রুটে অ্যাক্সেস করার সময় আমরা URL-এর শেষে ` index.html সাফিক্সটি দেখতে পাব না। এবং শেষটি হলো ডিফল্ট হ্যান্ডলার, যা অন্য সব URL ( /.* ) আমাদের Node.JS অ্যাপ্লিকেশনে রাউট করবে (অর্থাৎ, অ্যাপ্লিকেশনের "ডাইনামিক" অংশে, যা আমাদের বর্ণিত স্ট্যাটিক অ্যাসেটগুলোর বিপরীত)।

চলুন এখন ক্লাউড রান সার্ভিসের ওয়েব এপিআই ইউআরএলটি আপডেট করি।

appengine-frontend/ ডিরেক্টরিতে, আমাদের ক্লাউড রান-ভিত্তিক REST API-এর URL নির্দেশকারী এনভায়রনমেন্ট ভেরিয়েবল আপডেট করতে নিম্নলিখিত কমান্ডটি চালান:

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

অথবা app.yaml এ থাকা CHANGE_ME স্ট্রিংটি সঠিক URL দিয়ে ম্যানুয়ালি পরিবর্তন করুন:

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 ব্যবহার করে চালাতে চাই। বইয়ের ISBN কোড যাচাই করার জন্য আমরা Express ফ্রেমওয়ার্ক এবং isbn3 NPM মডিউলের উপর নির্ভর করি।

ডেভেলপমেন্ট ডিপেন্ডেন্সিতে, আমরা ফাইলের পরিবর্তন নিরীক্ষণ করার জন্য 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 ফাইলে রিডাইরেক্ট করবে। যেহেতু ডেভেলপমেন্ট মোডে আমরা অ্যাপ ইঞ্জিন রানটাইমের মধ্যে কাজ করছি না, তাই অ্যাপ ইঞ্জিনের ইউআরএল রাউটিং এখানে হয় না। এজন্য এর পরিবর্তে, আমরা এখানে সরাসরি রুট ইউআরএলটিকে এইচটিএমএল ফাইলে রিডাইরেক্ট করছি।

আমরা যে দ্বিতীয় এন্ডপয়েন্টটি /webapi নির্ধারণ করব, সেটি আমাদের ক্লাউড রান রেস্ট এপিআই-এর ইউআরএল ফেরত দেবে। এর ফলে, ক্লায়েন্ট-সাইড জাভাস্ক্রিপ্ট কোড জানতে পারবে বইয়ের তালিকা পেতে কোথায় কল করতে হবে।

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

সবশেষে, আমরা এক্সপ্রেস ওয়েব অ্যাপটি চালাচ্ছি এবং ডিফল্টরূপে এটি ৮০৮০ পোর্টে লিসেন করছে।

index.html পৃষ্ঠা

আমরা এই দীর্ঘ 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">

প্রথম দুটি লাইন Shoelace ওয়েব কম্পোনেন্ট লাইব্রেরি (একটি স্ক্রিপ্ট এবং একটি স্টাইলশিট) ইম্পোর্ট করে।

পরবর্তী লাইনে বইয়ের ISBN কোডগুলোর বারকোড তৈরি করার জন্য JsBarcode লাইব্রেরিটি ইম্পোর্ট করা হয়।

শেষের লাইনগুলো আমাদের নিজস্ব জাভাস্ক্রিপ্ট কোড এবং সিএসএস স্টাইলশিট ইম্পোর্ট করছে, যেগুলো আমাদের public/ সাবডিরেক্টরিতে অবস্থিত।

HTML পেজের body , আমরা Shoelace কম্পোনেন্টগুলো তাদের নিজস্ব এলিমেন্ট ট্যাগসহ ব্যবহার করি, যেমন:

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

অনেক HTML হয়েছে, আমাদের কোড পর্যালোচনার কাজ প্রায় শেষ। আর একটি মাত্র গুরুত্বপূর্ণ অংশ বাকি আছে: app.js নামের ক্লায়েন্ট-সাইড জাভাস্ক্রিপ্ট কোড, যা আমাদের REST API-এর সাথে যোগাযোগ করে।

app.js ক্লায়েন্ট-সাইড জাভাস্ক্রিপ্ট কোড

আমরা একটি শীর্ষ-স্তরের ইভেন্ট লিসেনার দিয়ে শুরু করি যা DOM কন্টেন্ট লোড হওয়ার জন্য অপেক্ষা করে:

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-এর URL-টি ফেচ করব। এর জন্য ধন্যবাদ আমাদের App Engine নোড কোডকে, যা app.yaml এ প্রাথমিকভাবে সেট করা এনভায়রনমেন্ট ভ্যারিয়েবলটি রিটার্ন করে। জাভাস্ক্রিপ্ট ক্লায়েন্ট-সাইড কোড থেকে /webapi এন্ডপয়েন্টটিকে কল করার ফলে, আমাদের ফ্রন্টএন্ড কোডে REST API URL-টি হার্ডকোড করতে হয়নি।

এছাড়াও আমরা একটি 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 URL, বর্তমান পেজ এবং নির্বাচিত ভাষা পাস করে 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 কল করার জন্য সঠিক URL-টি তৈরি করছি। সাধারণত আমরা তিনটি কোয়েরি প্যারামিটার উল্লেখ করতে পারি, কিন্তু এই UI-তে আমরা কেবল দুটি উল্লেখ করব:

  • page — একটি পূর্ণসংখ্যা যা বইগুলোর পৃষ্ঠাঙ্কনের বর্তমান পৃষ্ঠা নির্দেশ করে,
  • language — লিখিত ভাষা অনুযায়ী ফিল্টার করার জন্য একটি ভাষার স্ট্রিং।

এরপর আমরা Fetch API ব্যবহার করে আমাদের বইয়ের বিবরণ সম্বলিত 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 লাইব্রেরি ব্যবহার করে আসল বইয়ের পেছনের মলাটের মতো একটি চমৎকার বারকোড তৈরি করি!

অ্যাপ্লিকেশনটি স্থানীয়ভাবে চালানো এবং পরীক্ষা করা

এখনকার জন্য কোড যথেষ্ট, এবার অ্যাপ্লিকেশনটি বাস্তবে কাজ করতে দেখার পালা। আসল ডেপ্লয়মেন্ট করার আগে, আমরা প্রথমে ক্লাউড শেলের মধ্যে স্থানীয়ভাবে এটি পরীক্ষা করে দেখব।

আমাদের অ্যাপ্লিকেশনের জন্য প্রয়োজনীয় NPM মডিউলগুলো আমরা এভাবে ইনস্টল করি:

$ npm install

এবং আমরা হয় অ্যাপটি স্বাভাবিকভাবে চালাই:

$ npm start

অথবা nodemon এর সৌজন্যে পরিবর্তনসমূহ স্বয়ংক্রিয়ভাবে পুনরায় লোড হওয়ার মাধ্যমে, নিম্নরূপে:

$ npm run dev

অ্যাপ্লিকেশনটি স্থানীয়ভাবে চলছে এবং আমরা ব্রাউজার থেকে http://localhost:8080 ঠিকানায় এটি অ্যাক্সেস করতে পারি।

অ্যাপ ইঞ্জিন অ্যাপ্লিকেশনটি স্থাপন করা হচ্ছে

এখন যেহেতু আমরা নিশ্চিত যে আমাদের অ্যাপ্লিকেশনটি স্থানীয়ভাবে ঠিকঠাক চলছে, তাই এটিকে অ্যাপ ইঞ্জিনে ডেপ্লয় করার সময় এসেছে।

অ্যাপ্লিকেশনটি ডেপ্লয় করার জন্য, নিচের কমান্ডটি চালু করুন:

$ gcloud app deploy -q

প্রায় এক মিনিট পর অ্যাপ্লিকেশনটি ডেপ্লয় হয়ে যাবে।

অ্যাপ্লিকেশনটি https://${GOOGLE_CLOUD_PROJECT}.appspot.com এই ধরনের একটি ইউআরএল-এ পাওয়া যাবে।

আমাদের অ্যাপ ইঞ্জিন ওয়েব অ্যাপ্লিকেশনের UI অন্বেষণ করা হচ্ছে

এখন আপনি পারবেন:

  • আরও বই লোড করতে [More books...] বোতামে ক্লিক করুন।
  • শুধুমাত্র সেই ভাষার বই দেখতে একটি নির্দিষ্ট ভাষা নির্বাচন করুন।
  • সমস্ত বইয়ের তালিকায় ফিরে আসার জন্য, আপনি সিলেক্ট বক্সের ছোট ক্রস চিহ্নটিতে ক্লিক করে নির্বাচনটি বাতিল করতে পারেন।

১০. পরিষ্কার করা (ঐচ্ছিক)

যদি আপনি অ্যাপটি রাখতে না চান, তাহলে খরচ বাঁচাতে এবং সার্বিকভাবে একজন ভালো ক্লাউড ব্যবহারকারী হতে পুরো প্রজেক্টটি ডিলিট করে রিসোর্সগুলো পরিষ্কার করতে পারেন:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

১১. অভিনন্দন!

REST API ডেভেলপমেন্টের কিছু ভালো ডিজাইন প্যাটার্ন অনুসরণ করে, আমরা ক্লাউড ফাংশনস, অ্যাপ ইঞ্জিন এবং ক্লাউড রানের সাহায্যে বিভিন্ন ওয়েব এপিআই এন্ডপয়েন্ট ও ওয়েব ফ্রন্টএন্ডকে উন্মুক্ত করতে এবং বইয়ের একটি লাইব্রেরি সংরক্ষণ, আপডেট ও ব্রাউজ করার জন্য একগুচ্ছ সার্ভিস তৈরি করেছি।

আমরা যা আলোচনা করেছি

  • ক্লাউড ফাংশন
  • ক্লাউড ফায়ারস্টোর
  • ক্লাউড রান
  • অ্যাপ ইঞ্জিন

আরও এগিয়ে যাওয়া

আপনি যদি এই বাস্তব উদাহরণটি আরও গভীরভাবে খতিয়ে দেখতে ও এর পরিধি বাড়াতে চান, তাহলে এখানে এমন কিছু বিষয়ের তালিকা দেওয়া হলো যা আপনি অনুসন্ধান করতে পারেন:

  • ডেটা ইমপোর্ট ফাংশন এবং REST API কন্টেইনারের জন্য একটি সাধারণ API ফেসাড প্রদান করতে, API অ্যাক্সেস করার জন্য API কী পরিচালনা করার মতো বৈশিষ্ট্য যোগ করতে, অথবা API ব্যবহারকারীদের জন্য রেট সীমাবদ্ধতা নির্ধারণ করতে API Gateway- এর সুবিধা নিন।
  • REST API-এর ডকুমেন্টেশন ও পরীক্ষার ক্ষেত্র তৈরির জন্য App Engine অ্যাপ্লিকেশনে Swagger-UI নোড মডিউলটি স্থাপন করুন।
  • ফ্রন্টএন্ডে, বিদ্যমান ব্রাউজিং সুবিধার বাইরে, ডেটা সম্পাদনা এবং নতুন বইয়ের এন্ট্রি তৈরি করার জন্য অতিরিক্ত স্ক্রিন যোগ করুন। এছাড়াও, যেহেতু আমরা ক্লাউড ফায়ারস্টোর ডেটাবেস ব্যবহার করছি, তাই পরিবর্তন হওয়ার সাথে সাথে প্রদর্শিত বইয়ের ডেটা আপডেট করার জন্য এর রিয়েল-টাইম বৈশিষ্ট্যটি কাজে লাগান।