পাঠ ২৬ · ৫৮-এর মধ্যে · মডিউল ৬
Home / Courses / Full-Stack Web Frameworks / API ভার্সনিং

API ভার্সনিং স্ট্র্যাটেজি

API versioning strategies
৯ মিনিট পড়া মধ্যবর্তী · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • কেন API ভার্সনিং দরকার — backward compatibility রক্ষা করা
  • URL-path ভার্সনিং বনাম header-ভিত্তিক ভার্সনিং — কাজ করার পদ্ধতি ও trade-off
  • দুটো পদ্ধতিই বাস্তব Router হিসেবে বাস্তবায়ন করা
  • একই আন্ডারলাইং রেকর্ড থেকে দুই ভার্সনের জন্য দুই ভিন্ন response শেপ তৈরি করা

১ · কেন ভার্সনিং দরকার

একটি API চালু হওয়ার পর তার ক্লায়েন্ট (মোবাইল অ্যাপ, ফ্রন্ট-এন্ড, তৃতীয়-পক্ষ ইন্টিগ্রেশন) সেই response শেপের উপর নির্ভর করে কোড লেখে। যদি হঠাৎ কোনো ফিল্ডের নাম পাল্টানো হয় (যেমন full_name থেকে name), তাহলে পুরনো ক্লায়েন্ট কোড ভেঙে যাবে — কারণ সে এখনো full_name খুঁজছে। ভার্সনিং এই সমস্যা সমাধান করে: পুরনো শেপ (v1) এবং নতুন শেপ (v2) — দুটোই একসাথে সার্ভ করা হয়, পুরনো ক্লায়েন্ট নিজের সুবিধামতো সময়ে v2-তে মাইগ্রেট করতে পারে।

২ · দুটো প্রধান কৌশল

URL-path ভার্সনিং
/v1/users/5 বনাম /v2/users/5 — ভার্সন সরাসরি URL-এ। সুবিধা: স্পষ্ট, browser-এ সরাসরি টেস্ট করা যায়, cache-friendly। অসুবিধা: প্রতিটি ভার্সনের জন্য আলাদা রুট রেজিস্টার করতে হয়।
Header-ভিত্তিক ভার্সনিং
একই /users/5 URL, কিন্তু Accept-Version: v2 হেডার অনুযায়ী ভিন্ন শেপ ফেরত। সুবিধা: URL পরিষ্কার থাকে (resource identity বদলায় না)। অসুবিধা: browser-এ সরাসরি লিংক ক্লিক করে টেস্ট করা যায় না, হেডার সেট করা লাগে।

৩ · একই রেকর্ড, দুটো ভিন্ন response শেপ

নিচের কোড সেলে একটি ইউজার রেকর্ড আছে যেখানে full_name নামে একটি ফিল্ড আছে। v1 রেন্ডার ফাংশন সেটি অপরিবর্তিত রাখে, কিন্তু v2 রেন্ডার ফাংশন সেই একই তথ্য name কী (key) দিয়ে ফেরত দেয় (এবং একটি নতুন ফিল্ড joined_year যোগ করে) — এটি একটি বাস্তব, সাধারণ backward-incompatible পরিবর্তনের উদাহরণ যা ভার্সনিং ছাড়া পুরনো ক্লায়েন্টকে ভাঙত।

Python
users_db = {
    1: {"id": 1, "full_name": "Rahim Uddin", "email": "rahim@example.com", "joined_year": 2023},
}

def render_v1(user):
    """v1 শেপ -- আদি ফিল্ড নাম full_name বজায় রাখে"""
    return {"id": user["id"], "full_name": user["full_name"], "email": user["email"]}

def render_v2(user):
    """v2 শেপ -- full_name কে name-এ পাল্টানো হয়েছে, joined_year যোগ হয়েছে"""
    return {
        "id": user["id"],
        "name": user["full_name"],
        "email": user["email"],
        "joined_year": user["joined_year"],
    }

# --- পদ্ধতি ১: URL-path ভিত্তিক ভার্সনিং ---
class PathVersionedRouter:
    def __init__(self):
        self.routes = {}

    def route(self, path):
        def decorator(fn):
            self.routes[path] = fn
            return fn
        return decorator

    def dispatch(self, path):
        handler = self.routes.get(path)
        if handler is None:
            return 404, {"error": "Not Found"}
        return 200, handler()

path_app = PathVersionedRouter()

@path_app.route('/v1/users/1')
def get_user_v1():
    return render_v1(users_db[1])

@path_app.route('/v2/users/1')
def get_user_v2():
    return render_v2(users_db[1])

status_v1_path, body_v1_path = path_app.dispatch('/v1/users/1')
status_v2_path, body_v2_path = path_app.dispatch('/v2/users/1')

# --- পদ্ধতি ২: header ভিত্তিক ভার্সনিং, একই পাথ ---
class HeaderVersionedRouter:
    def __init__(self):
        self.routes = {}

    def route(self, path):
        def decorator(fn):
            self.routes[path] = fn
            return fn
        return decorator

    def dispatch(self, path, headers):
        handler = self.routes.get(path)
        if handler is None:
            return 404, {"error": "Not Found"}
        version = headers.get("Accept-Version", "v1")
        return 200, handler(version)

header_app = HeaderVersionedRouter()

@header_app.route('/users/1')
def get_user(version):
    if version == "v2":
        return render_v2(users_db[1])
    return render_v1(users_db[1])

status_v1_header, body_v1_header = header_app.dispatch('/users/1', {"Accept-Version": "v1"})
status_v2_header, body_v2_header = header_app.dispatch('/users/1', {"Accept-Version": "v2"})

print("== URL-path ভিত্তিক ভার্সনিং ==")
print(f"GET /v1/users/1               -> {status_v1_path} {body_v1_path}")
print(f"GET /v2/users/1               -> {status_v2_path} {body_v2_path}")

print("\n== Header ভিত্তিক ভার্সনিং (একই পাথ /users/1) ==")
print(f"GET /users/1, Accept-Version: v1 -> {status_v1_header} {body_v1_header}")
print(f"GET /users/1, Accept-Version: v2 -> {status_v2_header} {body_v2_header}")

print(f"\nযাচাই -- দুই পদ্ধতির v1 রেসপন্স শেপ অভিন্ন: {body_v1_path == body_v1_header}")
print(f"যাচাই -- দুই পদ্ধতির v2 রেসপন্স শেপ অভিন্ন: {body_v2_path == body_v2_header}")
print(f"যাচাই -- v1-এ 'full_name' আছে ও v2-তে নেই: {'full_name' in body_v1_path and 'full_name' not in body_v2_path}")
print(f"যাচাই -- v2-এ 'name' আছে ও v1-এ নেই: {'name' in body_v2_path and 'name' not in body_v1_path}")

    
লক্ষ্য করুন — users_db-এ আন্ডারলাইং ডেটা একটিই, কিন্তু ভার্সন অনুযায়ী রেসপন্সের শেপ ভিন্ন: v1-এ full_name কী আছে, v2-তে সেটি নেই বরং name আছে (সাথে নতুন joined_year)। দুই আলাদা রাউটিং কৌশল (URL-path এবং header) একই v1/v2 রেন্ডার ফাংশন ব্যবহার করেও হুবহু একই আউটপুট শেপ দেয় — যাচাই প্রিন্টগুলো এটি নিশ্চিত করে।
মূল কথা · Key takeaway

ভার্সনিং মানে শুধু "নতুন ফিচার যোগ করা" নয় — এটি বিশেষভাবে breaking change (যেমন ফিল্ড রিনেম) থেকে পুরনো ক্লায়েন্টকে রক্ষা করার কৌশল। URL-path ভার্সনিং সরল ও discoverable, header-ভিত্তিক ভার্সনিং URL-কে "খাঁটি রিসোর্স আইডেন্টিফায়ার" হিসেবে রাখে — কোনটি ব্যবহার করবেন তা নির্ভর করে টিমের priorities (সরলতা বনাম URL cleanliness)-এর উপর, কিন্তু দুটোরই লক্ষ্য এক: পুরনো ও নতুন ক্লায়েন্ট একসাথে সচল রাখা।

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

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

প্র ০১ যদি ভার্সনিং না করে সরাসরি full_name-কে name-এ রিনেম করে দেওয়া হতো, তাহলে কী হতো?

যেসব পুরনো ক্লায়েন্ট কোড response["full_name"] পড়ার জন্য লেখা হয়েছিল, তারা হঠাৎ KeyError (বা JavaScript-এ undefined) পেতে শুরু করত, কারণ সেই কী আর রেসপন্সে নেই। এটি একটি breaking change — সার্ভার-সাইড পরিবর্তনের কারণে ক্লায়েন্ট-সাইড কোড ভেঙে যাওয়া, যা ভার্সনিং এড়াতে সাহায্য করে (v1 আগের মতোই থেকে যায়, শুধু v2 নতুন শেপ ব্যবহার করে)।

প্র ০২ উপরের HeaderVersionedRouter.dispatch()-এ headers.get("Accept-Version", "v1") লেখা কেন — ডিফল্ট হিসেবে "v1" কেন বেছে নেওয়া হলো?

কারণ কোনো ক্লায়েন্ট যদি Accept-Version হেডারই না পাঠায় (পুরনো ক্লায়েন্ট, যেটি ভার্সনিং সম্পর্কে জানেই না), তাহলে তাকে নিরাপদ, পরিচিত পুরনো শেপ (v1) দেওয়াই যুক্তিসঙ্গত — হঠাৎ v2-তে পাঠিয়ে দিলে সেই ক্লায়েন্ট ভেঙে যেত। ডিফল্ট সবসময় "সবচেয়ে পুরনো, সবচেয়ে ব্যাপক সাপোর্টেড" ভার্সন হওয়া উচিত।

প্র ০৩ উপরের কোডে দুটো ভিন্ন Router ক্লাস (PathVersionedRouter, HeaderVersionedRouter) ব্যবহার করেও কেন v1 আউটপুট দুটো হুবহু মেলে?

কারণ দুই Router-ই শেষ পর্যন্ত একই render_v1(users_db[1]) ফাংশন কল করে — রাউটিং কৌশল (URL-এ ভার্সন লেখা বনাম হেডার পড়া) শুধু ঠিক করে কোন রেন্ডার ফাংশনটি কল হবে, কিন্তু রেন্ডারিং লজিক নিজেই দুই কৌশলে অভিন্ন থাকে। এটাই দেখায় ভার্সনিং কৌশলটি "রাউটিং" স্তরের সিদ্ধান্ত, রেসপন্স-শেপ তৈরির লজিক থেকে সম্পূর্ণ আলাদা।

অনুশীলন

  1. চিন্তা করুন: একটি মোবাইল অ্যাপের API-র জন্য URL-path ভার্সনিং বেশি ব্যবহারিক নাকি header-ভিত্তিক — কেন?

    দুটোই ব্যবহারিক হতে পারে, তবে URL-path ভার্সনিং সাধারণত সহজ ও ডিবাগ করা সহজ — ডেভেলপার সরাসরি URL দেখেই বুঝতে পারে কোন ভার্সন কল হচ্ছে, লগ ফাইলেও ভার্সন স্পষ্ট থাকে, এবং CDN/proxy লেভেলে ভার্সন অনুযায়ী cache/route করা সহজ। header-ভিত্তিক ভার্সনিং তখন বেশি উপযোগী যখন URL-কে "খাঁটি রিসোর্স আইডেন্টিফায়ার" (একটি রিসোর্সের একটিই URL) রাখা গুরুত্বপূর্ণ — যেমন hypermedia/HATEOAS-ভিত্তিক API-তে (L27 দেখুন)।

  2. পরীক্ষা করুন: উপরের কোড সেলে render_v2()-এ আরেকটি ফিল্ড "account_status": "active" যোগ করুন, তারপর আবার চালিয়ে দেখুন v1 আউটপুট (যেটি এখনো render_v1() কল করে) পরিবর্তিত হয় কি না।

    v1 আউটপুট একদম অপরিবর্তিত থাকবে, কারণ render_v1() ফাংশনে কোনো পরিবর্তন করা হয়নি — শুধু render_v2()-এ নতুন ফিল্ড যোগ হয়েছে। এটাই ভার্সনিং-এর মূল সুবিধা বাস্তবে দেখায়: v2-তে নতুন ফিচার (নতুন ফিল্ড) যোগ করা v1-এর উপর নির্ভরশীল কোনো পুরনো ক্লায়েন্টকে বিন্দুমাত্র প্রভাবিত করে না।

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

  • কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ আর্কিটেকচার প্যাটার্ন, ফ্রন্ট-এন্ড/ব্যাক-এন্ড ফ্রেমওয়ার্ক ফান্ডামেন্টাল, স্টেট ম্যানেজমেন্ট, REST API, ORM, অথেন্টিকেশন, রেন্ডারিং স্ট্র্যাটেজি ও ডিপ্লয়মেন্ট — বাকি পাঠগুলো শীঘ্রই যুক্ত হবে।
  • পূর্ববর্তী পাঠ: পেজিনেশন, ফিল্টারিং ও সর্টিং L25 এই পাঠের Router-ভিত্তিক ভার্সনিং কৌশল আগের পাঠগুলোর CRUD ও কুয়েরি প্যাটার্নের সাথে একই "রুট → হ্যান্ডলার" ভিত্তির উপর গড়া।
  • সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics, System Design, Cybersecurity, Cloud Computing & DevOps, Computer Networks, Operating Systems, Computer Architecture, Programming Languages & Compiler Design, Software Engineering & Git, Theory of Computation, Engineering Economics ও Full-Stack Web Frameworks — সব এক জায়গায়।
আগের পাঠ
পেজিনেশন, ফিল্টারিং ও সর্টিং