পাঠ ২৭ · ৫৮-এর মধ্যে · মডিউল ৬

HATEOAS ও API ডকুমেন্টেশন (OpenAPI/Swagger)

HATEOAS & API documentation (OpenAPI/Swagger)
৯ মিনিট পড়া মধ্যবর্তী · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • HATEOAS কী এবং কেন এটি "কাঁচা ডেটা API"-র চেয়ে বেশি self-descriptive
  • একটি রিসোর্সের অবস্থা অনুযায়ী গণনা-করা _links ডিকশনারি বাস্তবে যুক্ত করা
  • দুটো ভিন্ন-অবস্থার রিসোর্সে সঠিকভাবে ভিন্ন লিংক-সেট দেখানো, যাচাইসহ
  • OpenAPI/Swagger মেশিন-পাঠযোগ্য ডকুমেন্টেশন হিসেবে কী ভূমিকা রাখে

১ · HATEOAS — লিংকসহ রেসপন্স

একটি সাধারণ (non-HATEOAS) API রেসপন্স শুধু ডেটা ফেরত দেয়: {"id": 1, "status": "pending", "total": 1200}। ক্লায়েন্টকে তখন আগে থেকেই জানতে হয় এই অর্ডার বাতিল করতে কোন URL-এ, কোন মেথডে কল করতে হবে — এই তথ্য ক্লায়েন্টের কোডে হার্ডকোড করা থাকে। HATEOAS এই সমস্যার সমাধান করে: রেসপন্সেই একটি _links অংশ যুক্ত থাকে যা বলে দেয় এই মুহূর্তে এই রিসোর্সে কী কী কাজ সম্ভব এবং কোন URL/মেথডে — ক্লায়েন্টকে আর URL হার্ডকোড করতে হয় না, শুধু লিংক অনুসরণ করলেই চলে।

কাঁচা ডেটা API
ক্লায়েন্ট নিজে জানে/অনুমান করে যে cancel করতে হলে POST /orders/1/cancel কল করতে হবে — এই যুক্তি ক্লায়েন্ট কোডে হার্ডকোড করা।
HATEOAS API
রেসপন্সের _links.cancel.href সরাসরি সঠিক URL বলে দেয় — এবং এই লিংক তখনই থাকে যখন cancel করা বাস্তবিক সম্ভব।

২ · অবস্থা অনুযায়ী লিংক গণনা করা

নিচের কোড সেলে attach_links() ফাংশন একটি অর্ডার dict নিয়ে তার status পরীক্ষা করে নির্ধারণ করে কোন কোন লিংক প্রাসঙ্গিক। pending অর্ডারে বাতিল (cancel) ও আপডেট (update) সম্ভব, কিন্তু shipped অর্ডারে সেগুলো আর সম্ভব নয় — বরং একটি ট্র্যাকিং (track) লিংক প্রাসঙ্গিক হয়ে ওঠে।

Python
orders = {
    1: {"id": 1, "status": "pending", "total": 1200},
    2: {"id": 2, "status": "shipped", "total": 850},
}

def attach_links(order):
    """অর্ডারের বর্তমান অবস্থা দেখে প্রাসঙ্গিক হাইপারলিংক গণনা করে যুক্ত করে"""
    base = f"/orders/{order['id']}"
    links = {
        "self": {"href": base, "method": "GET"},
        "items": {"href": f"{base}/items", "method": "GET"},
    }

    if order["status"] == "pending":
        links["cancel"] = {"href": f"{base}/cancel", "method": "POST"}
        links["update"] = {"href": base, "method": "PATCH"}
    elif order["status"] == "shipped":
        links["track"] = {"href": f"{base}/tracking", "method": "GET"}

    order_with_links = dict(order)
    order_with_links["_links"] = links
    return order_with_links

order1 = attach_links(orders[1])
order2 = attach_links(orders[2])

print("== অর্ডার ১ (pending) ==")
print(order1)

print("\n== অর্ডার ২ (shipped) ==")
print(order2)

print("\n== যাচাই ==")
print(f"pending অর্ডারে 'cancel' লিংক আছে:      {'cancel' in order1['_links']}")
print(f"shipped অর্ডারে 'cancel' লিংক নেই:       {'cancel' not in order2['_links']}")
print(f"shipped অর্ডারে 'track' লিংক আছে:        {'track' in order2['_links']}")
print(f"pending অর্ডারে 'track' লিংক নেই:        {'track' not in order1['_links']}")
print(f"দুই অর্ডারেই 'self' ও 'items' লিংক আছে:  "
      f"{all(k in order1['_links'] for k in ('self', 'items')) and all(k in order2['_links'] for k in ('self', 'items'))}")

    
order1["_links"]-এ চারটি লিংক থাকে — self, items, cancel, update — কারণ status "pending"। order2["_links"]-এ থাকে self, items, track — cancel/update নেই, কারণ একটি শিপড অর্ডার আর বাতিল বা আপডেট করা যায় না। এই লিংক-সেট গণনা করা হয়েছে order["status"] পরীক্ষা করে — হার্ডকোড করা কোনো ফিক্সড তালিকা নয়।

৩ · OpenAPI/Swagger — মেশিন-পাঠযোগ্য ডকুমেন্টেশন

OpenAPI (আগে Swagger নামে পরিচিত ছিল) একটি স্ট্যান্ডার্ড, YAML বা JSON ফরম্যাটে লেখা স্পেসিফিকেশন যা একটি API-র প্রতিটি endpoint, তার parameter, request body শেপ, এবং সম্ভাব্য response শেপ/status code বর্ণনা করে — মানুষের পড়ার জন্য প্রোজ ডকুমেন্টেশনের বদলে, এটি এমন একটি কাঠামোবদ্ধ ফাইল যা টুল দিয়ে পার্স করা যায়। এই একটি ফাইল থেকে স্বয়ংক্রিয়ভাবে তৈরি করা যায়:

  • ইন্টারঅ্যাক্টিভ ডকুমেন্টেশন (Swagger UI) — ব্রাউজারে সরাসরি API টেস্ট করার UI
  • ক্লায়েন্ট SDK — বিভিন্ন ভাষার জন্য টাইপ-সচেতন API ক্লায়েন্ট কোড
  • রিকোয়েস্ট ভ্যালিডেশন — ইনকামিং রিকোয়েস্ট স্পেসিফিকেশনের সাথে না মিললে আগেভাগেই ধরা

এই পাঠের সুযোগের বাইরে বাস্তব OpenAPI YAML তৈরি করা, কিন্তু মূল ধারণাটি বোঝা জরুরি — HATEOAS রানটাইমে "এই মুহূর্তে কী করা যায়" জানায়, আর OpenAPI ডিজাইন-টাইমে/ডকুমেন্টেশনে "এই API সামগ্রিকভাবে কী কী করতে পারে" জানায় — দুটো ভিন্ন স্তরে একে অপরের পরিপূরক।

মূল কথা · Key takeaway

HATEOAS মানে প্রতিটি রেসপন্স নিজেই বলে দেয় এই মুহূর্তে কোন কাজ সম্ভব, রিসোর্সের বর্তমান অবস্থা অনুযায়ী গণনা করে — ক্লায়েন্টকে URL হার্ডকোড করতে হয় না। OpenAPI/Swagger এর পরিপূরক একটি ভিন্ন স্তরের টুল — পুরো API-র কাঠামো ডিজাইন-টাইমে বর্ণনা করে মেশিন-পাঠযোগ্যভাবে, যাতে ডকুমেন্টেশন, SDK ও ভ্যালিডেশন স্বয়ংক্রিয়ভাবে তৈরি করা যায়। M6-এর পাঁচটি পাঠ (L23-L27) একসাথে একটি সম্পূর্ণ, ভালোভাবে-ডিজাইন করা REST API-র মূল ভিত্তি তৈরি করল — এবার M7-এ আমরা এই API-র পেছনে থাকা ডেটা ORM দিয়ে কীভাবে মডেল করা হয় তা শিখব।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ একটি অর্ডার যদি "delivered" অবস্থায় থাকে (উপরের কোডে নেই), তাহলে attach_links()-এ সেটি কী কী লিংক পাবে?

বর্তমান কোড অনুযায়ী "delivered" স্ট্যাটাসের জন্য if/elif-এর কোনো শাখা মেলে না (শুধু "pending" ও "shipped" পরীক্ষা করা হয়েছে), তাই সেই অর্ডার শুধু ডিফল্ট লিংক দুটো পাবে — self ও items, কোনো action-লিংক (cancel/update/track) ছাড়াই। বাস্তব সিস্টেমে সাধারণত এখানে আরেকটি elif order["status"] == "delivered": শাখা যোগ করে হয়তো একটি review বা return (ফেরত পাঠানো) লিংক দেওয়া হতো।

প্র ০২ HATEOAS ব্যবহার করলে ক্লায়েন্ট কোডে কী সুবিধা হয় যদি ভবিষ্যতে সার্ভার /orders/1/cancel URL পাল্টে /orders/1/void করে দেয়?

যদি ক্লায়েন্ট URL হার্ডকোড না করে বরং সবসময় response["_links"]["cancel"]["href"] থেকে URL নিয়ে কল করে, তাহলে সার্ভার URL পাল্টালেও ক্লায়েন্টের কোনো কোড পরিবর্তন লাগে না — পরের বার রেসপন্স আসার সাথে সাথেই নতুন href পাওয়া যাবে এবং ক্লায়েন্ট স্বয়ংক্রিয়ভাবে সঠিক নতুন URL ব্যবহার করবে। এটিই HATEOAS-এর মূল প্রতিশ্রুতি — URL গঠন ক্লায়েন্ট ও সার্ভারের মধ্যে একটি "চুক্তি" না হয়ে, রানটাইমে আবিষ্কারযোগ্য (discoverable) হয়ে যায়।

প্র ০৩ OpenAPI স্পেসিফিকেশন কি HATEOAS-এর "রিপ্লেসমেন্ট" — একটি থাকলে অন্যটি লাগে না?

না, দুটো ভিন্ন স্তরে কাজ করে। OpenAPI ডিজাইন-টাইম/ডকুমেন্টেশন টুল — এটি বলে "এই API-তে এই endpoint-গুলো থাকতে পারে", কিন্তু এটি জানে না কোনো নির্দিষ্ট মুহূর্তে কোনো নির্দিষ্ট রিসোর্সের জন্য কোন কাজ বাস্তবে সম্ভব। HATEOAS রানটাইম আচরণ — প্রতিটি রেসপন্সে এই মুহূর্তে, এই রিসোর্সের জন্য ঠিক কী সম্ভব তা বলে দেয় (যেমন এই পাঠের pending/shipped উদাহরণ)। বাস্তবে অনেক API দুটোই ব্যবহার করে — OpenAPI দিয়ে সামগ্রিক ডকুমেন্টেশন, আর প্রয়োজনে HATEOAS দিয়ে রানটাইম discoverability।

অনুশীলন

  1. চিন্তা করুন: একটি ব্লগ পোস্ট রিসোর্সে (draft বনাম published অবস্থা) HATEOAS লিংক কীভাবে অবস্থাভেদে ভিন্ন হতে পারে?

    একটি draft পোস্টে হয়তো publish লিংক (POST) এবং edit লিংক (PATCH) থাকবে, কিন্তু একটি published পোস্টে publish লিংক থাকবে না (এটি ইতিমধ্যে প্রকাশিত), বরং হয়তো unpublish বা view লিংক থাকবে — ঠিক যেমন এই পাঠে pending অর্ডারে cancel ছিল কিন্তু shipped অর্ডারে ছিল না।

  2. পরীক্ষা করুন: উপরের কোড সেলে orders-এ একটি তৃতীয় অর্ডার 3: {"id": 3, "status": "cancelled", "total": 500} যোগ করুন, তারপর attach_links(orders[3]) কল করে দেখুন এটি কোন কোন লিংক পায়।

    যেহেতু "cancelled" status-এর জন্য কোনো if/elif শাখা মেলে না, এই অর্ডারও শুধু ডিফল্ট self ও items লিংক পাবে — cancel, update, বা track কোনোটিই না, যা যুক্তিসঙ্গত (ইতিমধ্যে বাতিল হওয়া অর্ডার আবার বাতিল বা আপডেট করা যায় না)।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

আগের পাঠ
API ভার্সনিং স্ট্র্যাটেজি