API versioning: ব্যবহারকারী না ভেঙে breaking change আনুন

Response-এর একটি field বদলালেই পুরোনো অ্যাপ ভেঙে যায়। কোন বদল breaking, কীভাবে version দেবেন ও পুরোনো version ভদ্রভাবে বন্ধ করবেন তা শিখুন।

পাশাপাশি দুটি API version v1 ও v2 ভিন্ন response আকৃতি দিচ্ছে

আপনি একটি API প্রকাশ করেছেন এবং মানুষ তার ওপর অ্যাপ বানিয়েছে। এখন একটি field-এর নাম বদলাতে চান, আর বদলানো মাত্র তাদের অ্যাপ ভেঙে যায়, অথচ আপনি সেগুলো দেখতেও পান না। Versioning হলো নির্ভরশীলদের চমকে না দিয়ে API উন্নত করার উপায়। নিচের server, client ও ফল প্রকাশের আগে Python 3-এ চালানো হয়েছে।

Breaking কি না?

সাধারণত নিরাপদ (non-breaking):

  • নতুন endpoint যোগ।
  • Response-এ নতুন ঐচ্ছিক field যোগ (client-এর অচেনা field উপেক্ষা করা উচিত)।
  • ঐচ্ছিক request parameter যোগ।

Breaking:

  • Field বা endpoint মুছে ফেলা বা নাম বদলানো।
  • Type বদলানো (সংখ্যা হয়ে গেল string)।
  • Field-এর অর্থ বা একক বদলানো।
  • ঐচ্ছিক parameter বাধ্যতামূলক করা।
  • Client যার ওপর নির্ভর করে সেই error ফরম্যাট বা status code বদলানো।

সেরা versioning কৌশল হলো নতুন version-এর দরকারই না পড়া: বদলানোর বদলে যোগ করুন। বদলটি সত্যিই breaking হলেই নতুন version বানান।

Version কোথায় রাখবেন

১. URL path-এ: /v1/users, /v2/users। দেখা, browser-এ পরীক্ষা, routing ও caching সহজ। সবচেয়ে প্রচলিত। ২. Header-এ: নিজস্ব header বা version-যুক্ত media type, যেমন Accept: application/vnd.example.v2+json। URL পরিষ্কার থাকে, কিন্তু দ্রুত চেষ্টা কঠিন ও ভুল হওয়া সহজ। ৩. Query parameter-এ: ?version=2। সরল, তবে versioning ও filtering মিশে যায়।

কোনোটিই নিখুঁত নয়। বেশিরভাগ দলের জন্য URL versioning ব্যবহারিক পছন্দ, কারণ সবাই বোঝে। নিচের উদাহরণ এটিই ব্যবহার করে।

একটি চালানো যায় এমন উদাহরণ

Version 1 একটি name দেয়। Version 2 তা first_name ও last_name-এ ভাগ করে, যা breaking change, তাই নতুন version দরকার। api.py নামে সংরক্ষণ করুন:

import json
import os
from http.server import BaseHTTPRequestHandler, HTTPServer

USERS = {1: {"first": "Rahim", "last": "Uddin", "email": "rahim@example.com"}}


def v1_user(u):
    # old shape: one "name" field
    return {"id": 1, "name": f"{u['first']} {u['last']}", "email": u["email"]}


def v2_user(u):
    # new shape: name split in two (a breaking change for v1 clients)
    return {"id": 1, "first_name": u["first"], "last_name": u["last"], "email": u["email"]}


ROUTES = {"/v1/users/1": v1_user, "/v2/users/1": v2_user}
SUNSET = {"/v1/": "Wed, 31 Dec 2026 23:59:59 GMT"}  # v1 retirement date


class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        build = ROUTES.get(self.path)
        if build is None:
            return self.send_json(404, {"error": "not found"})
        extra = {}
        for prefix, date in SUNSET.items():
            if self.path.startswith(prefix):
                extra = {
                    "Deprecation": "true",
                    "Sunset": date,
                    "Link": '</v2/users/1>; rel="successor-version"',
                }
        self.send_json(200, build(USERS[1]), extra)

    def send_json(self, status, data, headers=None):
        body = json.dumps(data).encode()
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        for k, v in (headers or {}).items():
            self.send_header(k, v)
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, *args):
        pass


if __name__ == "__main__":
    HTTPServer(("127.0.0.1", int(os.environ.get("PORT", "3931"))), Handler).serve_forever()

ছোট client, client.py (আগে server চালু করুন: PORT=3931 python3 api.py):

import json
import urllib.error
import urllib.request

BASE = "http://127.0.0.1:3931"


def get(path):
    try:
        r = urllib.request.urlopen(BASE + path)
    except urllib.error.HTTPError as e:
        r = e
    return r.status, dict(r.headers), json.load(r)


for path in ("/v1/users/1", "/v2/users/1", "/v3/users/1"):
    status, headers, body = get(path)
    print(path, status, body)
    for h in ("Deprecation", "Sunset", "Link"):
        if h in headers:
            print("   ", h + ":", headers[h])

ফল:

/v1/users/1 200 {'id': 1, 'name': 'Rahim Uddin', 'email': 'rahim@example.com'}
    Deprecation: true
    Sunset: Wed, 31 Dec 2026 23:59:59 GMT
    Link: </v2/users/1>; rel="successor-version"
/v2/users/1 200 {'id': 1, 'first_name': 'Rahim', 'last_name': 'Uddin', 'email': 'rahim@example.com'}
/v3/users/1 404 {'error': 'not found'}

ফল পড়া

  • v1 অপরিবর্তিত: পুরোনো অ্যাপ আগের মতোই name পায়।
  • v2-তে নতুন আকৃতি নতুন বা হালনাগাদ client-এর জন্য।
  • v1 অবসরের সংকেত বহন করে। Sunset একটি মানক HTTP header (RFC 8594), যা বলে endpoint কবে বন্ধ হওয়ার কথা। Deprecation বাস্তবে "বাতিলযোগ্য" চিহ্নিত করতে ব্যবহার হয়, আর rel="successor-version" সহ Link প্রতিস্থাপক দেখায়। Client ও monitoring টুল এগুলো স্বয়ংক্রিয়ভাবে পড়তে পারে।
  • অচেনা version পরিষ্কার 404 দেয়।

Test

test_api.py নামে সংরক্ষণ করুন:

import json
import threading
import unittest
import urllib.request
from http.server import HTTPServer

from api import Handler


class VersionTests(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.srv = HTTPServer(("127.0.0.1", 0), Handler)
        cls.port = cls.srv.server_address[1]
        threading.Thread(target=cls.srv.serve_forever, daemon=True).start()

    @classmethod
    def tearDownClass(cls):
        cls.srv.shutdown()

    def get(self, path):
        r = urllib.request.urlopen(f"http://127.0.0.1:{self.port}{path}")
        return r, json.load(r)

    def test_v1_shape_is_unchanged(self):
        _, body = self.get("/v1/users/1")
        self.assertEqual(set(body), {"id", "name", "email"})

    def test_v2_has_new_shape(self):
        _, body = self.get("/v2/users/1")
        self.assertEqual(set(body), {"id", "first_name", "last_name", "email"})

    def test_v1_is_marked_deprecated(self):
        r, _ = self.get("/v1/users/1")
        self.assertIn("Sunset", r.headers)

    def test_v2_has_no_sunset(self):
        r, _ = self.get("/v2/users/1")
        self.assertNotIn("Sunset", r.headers)


if __name__ == "__main__":
    unittest.main()
$ python3 -m unittest -v
Ran 4 tests in 0.544s

OK

প্রথম test-টি গুরুত্বপূর্ণ: এটি v1-এর আকৃতি আটকে রাখে। কেউ পরে v1 "উন্নত" করে পুরোনো client ভেঙে দিলে ব্যবহারকারী টের পাওয়ার আগেই test ব্যর্থ হবে।

বুদ্ধিমানের deprecation প্রক্রিয়া

১. আগেই জানান (docs, email, changelog), বাস্তব তারিখসহ; client-কে দিন কয়েক মাস, কয়েক দিন নয়। ২. সংকেত যোগ করুন: Deprecation ও Sunset header, migration guide-এর লিংক। ৩. Version অনুযায়ী ব্যবহার মাপুন। প্রতিটি request কোন version ব্যবহার করে (আর সম্ভব হলে কোন client) log করুন। কারা এখনো v1 ডাকে না জানলে নিরাপদে বন্ধ করা যায় না। ৪. তারিখের একটু আগে বাকি ব্যবহারকারীদের সঙ্গে যোগাযোগ করুন। ৫. বন্ধ করুন। কিছু দল আগে বিভ্রান্তিকর 404-এর বদলে সহায়ক বার্তাসহ 410 Gone দেয়।

Versioning-এর খরচ

প্রতিটি সচল version এমন কোড যা রক্ষণাবেক্ষণ, test ও নিরাপদ রাখতে হয়। একটি নিরাপত্তা সংশোধন v1, v2, v3 সবখানে দিতে হতে পারে। সমর্থিত version কম রাখুন। ভালো নকশায় মূল যুক্তি ভাগ করা থাকে এবং প্রতি version-এ পাতলা স্তর থাকে, যেমন v1_user / v2_user ফাংশন: দুটিই একই data পড়ে, শুধু উপস্থাপনায় ভিন্ন।

সাধারণ ভুল

  • "ছোট নাম বদল" ভেবে নতুন version ছাড়াই breaking change করা।
  • ছোট যোগ-করার বদলেও সবকিছুতে version দিয়ে অকারণ v2, v3, v4 বানানো।
  • পুরোনো version কখনো না সরানো, ফলে রক্ষণাবেক্ষণ খরচ চিরকাল বাড়ে।
  • সতর্কতা ও ব্যবহারের তথ্য ছাড়া version বন্ধ করা।
  • যুক্তি ভাগ না করে প্রতি version-এ পুরো কোডবেস কপি করা।
  • ভাগ করা কোড বদলের পর পুরোনো version test না করা।

অভ্যন্তরীণ API ও মোবাইল অ্যাপ

মোবাইল অ্যাপ সবচেয়ে কঠিন, কারণ পুরোনো অ্যাপ অনেক দিন ইনস্টল থাকে এবং ব্যবহারকারী আপনার সময়সূচিতে আপডেট করেন না। পুরোনো version বেশি দিন সচল রাখুন এবং খুব পুরোনো অ্যাপকে আপডেট করতে বলার ন্যূনতম-সমর্থিত-version পরীক্ষা ভাবুন। যে API শুধু আপনার একসঙ্গে deploy হওয়া web frontend ব্যবহার করে, তাতে প্রায়ই দুটি একসঙ্গে বদলে আনুষ্ঠানিক versioning বাদ দেওয়া যায়, তবে নিরাপদ ক্রমে deploy করুন (আগে backend, কিছুক্ষণ দুই আকৃতি সমর্থন করে)।

মূল কথা: যোগ-করা বদল পছন্দ করুন; বদল সত্যিই breaking হলে নতুন version দিন (URL versioning সবচেয়ে সরল), পুরোনোটি স্থিতিশীল রেখে test দিয়ে আটকান, তারিখ ঘোষণা করুন, header-এ সংকেত দিন, কে এখনো ব্যবহার করে মাপুন, তারপর বন্ধ করুন।


Backend সিরিজ, পর্ব ৩ (নির্ভরযোগ্য API): 4 নম্বর (মোট 5)। আগের লেখা: Background job ও queue: retry, backoff ও dead job কোডসহ.

আরও পড়ুন